Send and Sync Bounds
Send and Sync are the two marker traits that make Rust's threading safe, and you mostly never mention them. They are auto-implemented for anything built out of parts that already have them, and the compiler checks them at every thread boundary.
Sendmeans a value can be moved to another thread.Syncmeans&Tcan be shared with another thread. Equivalently,T: Syncexactly when&T: Send.
Rc<T> is neither. Its reference count is a plain non-atomic integer, so two threads bumping it at once would corrupt it, and the compiler simply refuses:
`Rc<i32>` cannot be sent between threads safely
Arc<T> is the same type with an atomic count, so it is Send + Sync whenever T is. Cell and RefCell are Send but not Sync: moving one is fine, sharing one is not, because their borrow tracking is not atomic either.
You start writing these bounds yourself the moment you write a generic helper that spawns:
pub fn run<T, F>(f: F) -> T
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,That is thread::spawn's own signature. The 'static is not "lives forever"; it is "contains no borrows of anything shorter", which is what spawn needs because the thread may outlive the caller. thread::scope is the escape hatch that drops it.
The closure trait matters too:
FnOnceis consumed by the call. A closure that moves out a capturedStringis onlyFnOnce.Fncan be called repeatedly through&self, which is what you need if one closure is shared by many threads.
And sharing a closure across threads is a Sync requirement, not a Send one: Arc<F> is only Send when F: Send + Sync.
Your Task
Write the bounds for two generic helpers. The bodies are ordinary; the where clauses are the exercise.
Implement parallel_map
pub fn parallel_map<T, U, F>(items: Vec<T>, f: F) -> Vec<U>Runs f on every item, each on its own thread, and returns the results in the same order as the input. One f is shared by all of the threads, so wrap it in an Arc and clone that per thread.
let doubled = parallel_map(vec![1, 2, 3], |n: i32| n * 2);
assert_eq!(doubled, vec![2, 4, 6]);Your bounds must be tight enough to compile and loose enough to accept non-Copy items and results, such as Vec<String> in and Vec<usize> out.
Implement run_jobs
pub fn run_jobs<T, F>(jobs: Vec<F>) -> Vec<T>Runs each job on its own thread and returns the results in job order. Each job is called exactly once and is moved rather than shared, so its bounds are looser than parallel_map's in one specific way. It must accept Box<dyn FnOnce() -> T + Send> and closures that consume what they captured.
Notes
- Do not add
Clone,Copy, orDebug. Anything you require, a caller has to satisfy, and the tests call these with types that do not have them. - Order comes from spawning everything first and joining afterwards, not from joining as you go.
handle.join()returns aResult; unwrapping it is fine here.
Hints
Click here to reveal hints
parallel_mapneedsT: Send + 'static,U: Send + 'static, andF: Fn(T) -> U + Send + Sync + 'static.- The
SynconFis whatArc<F>: Senddemands. Drop it and the error namesArc<F>rather thanF. run_jobsneedsT: Send + 'staticandF: FnOnce() -> T + Send + 'static. NoSync, because each job is moved into exactly one thread.- Collect the
JoinHandles into aVecbefore joining any of them..map(...).map(|h| h.join())in one chain joins each thread immediately and runs everything sequentially. jobs.into_iter().map(thread::spawn)is enough for the spawn half ofrun_jobs.
Run to execute your code and see test results here.