We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Expert Edge: Idiom, Review and Capstones step 6 of 14
Capstone: build a thread pool
Build a thread pool using the standard library. Queue jobs for its workers, let different workers run jobs concurrently, and finish all queued work before the pool is dropped.
pub fn pool_run(jobs: Vec<u64>, workers: usize) -> Vec<u64>
Keep the supplied work and pool_run functions. Implement Worker,
ThreadPool::new, ThreadPool::execute and Drop for ThreadPool. You may
change their fields to support ownership and shutdown.
Required behavior
- Create
workersthreads, or one thread whenworkersis zero. - Queue each submitted closure for exactly one execution. Do not hold the receiver lock while executing a job.
- On drop, stop accepting jobs, finish the queued jobs and join every worker. An empty pool must also shut down without hanging.
- Return one answer per input, preserving order and duplicates. Empty input returns an empty vector.
work counts Collatz steps until the value is at most one. Each step
divides an even value by two, or replaces an odd value with 3 * n + 1.
Both zero and one take zero steps. For example, [0, 1, 3, 6] produces
[0, 0, 7, 8], regardless of the worker count.
This exercise uses terminating inputs whose intermediate Collatz values fit
in u64, and worker counts small enough for the runtime to create the
threads. It does not ask you to prove Collatz termination, handle arbitrary
overflowing sequences or recover from thread-creation failure. Jobs are
assumed to finish without panicking. A production pool would need an
explicit policy for panics, cancellation and resource limits.
Sending different closures through one channel
The provided job type is Box<dyn FnOnce() + Send + 'static>:
FnOncepermits a closure to consume captured values when called.Box<dyn ...>lets the channel hold closures with different concrete types.Sendpermits transferring a job to a worker thread.'staticrules out captures that borrow shorter-lived data. Owned values and static references are allowed; the job itself need not live forever.
A boxed FnOnce is directly callable. This complete example moves a string
into a job, then consumes it when the job runs:
fn main() {
let text = String::from("queued work");
let job: Box<dyn FnOnce() + Send + 'static> = Box::new(move || {
assert_eq!(text, "queued work");
drop(text);
});
job();
}
Store the receiving end of the job channel in Arc<Mutex<mpsc::Receiver<Job>>>.
The receiver is not cloneable or Sync. The mutex gives one worker at a time
access to it, and each worker owns a clone of the Arc.
Release the lock before running a job
Each worker repeats four steps: lock the receiver, receive a job, release the lock, then run the job. Use a separate block for the lock guard so its scope ends before the call to the closure.
The following complete example demonstrates that scope with a channel of integers. It checks that the mutex is available inside the message handler:
use std::sync::{Mutex, mpsc};
fn main() {
let (sender, receiver) = mpsc::channel();
sender.send(7).expect("receiver is alive");
drop(sender);
let receiver = Mutex::new(receiver);
loop {
let message = {
let guard = receiver.lock().expect("receiver mutex poisoned");
guard.recv()
};
match message {
Ok(value) => {
assert_eq!(value, 7);
assert!(receiver.try_lock().is_ok());
}
Err(_) => break,
}
}
}
Be careful with while let Ok(job) = rx.lock().expect("lock").recv().
Its temporary guard remains alive through the successful iteration's body.
Calling job() there holds the receiver lock during the job, preventing
other workers from receiving work. The guard is released at the end of
that iteration, not held once for the entire loop.
Close the channel before joining
Dropping the final sender disconnects the channel. Workers can still receive
messages already queued; recv() returns an error once the queue is empty.
Use that error to exit the worker loop.
The pool must release its sender before joining workers. Otherwise an idle
worker can wait in recv() while the pool waits in join(), with neither
able to continue. An extra sender clone kept elsewhere would also prevent
disconnection. A timeout may indicate this deadlock, but it can have other
causes too.
Taking ownership inside Drop
Drop::drop receives &mut self, while JoinHandle::join consumes its
handle. The starter tries to move a handle out through a mutable reference,
producing E0507.
Store each handle in an Option. Calling .take() moves the handle out
and leaves None behind, so you can join the returned handle. Store the
sender in an Option for the same reason: take and drop it first, then
take and join each worker handle.
Output order and concurrency checks
Workers may finish jobs in any order. The supplied pool_run stores each
result in a slot identified by its input index. It reads those slots after
dropping the pool, when all workers should have finished.
The output tests check answers and ordering, including the same batch with one and eight workers. They do not by themselves prove that jobs overlap: an accidentally serialized pool can produce the same answers. As an extra check, submit two jobs to a two-worker pool and have each wait at a shared barrier. They must both reach the barrier for either to proceed. Run this check with a timeout so a broken pool cannot hang your test indefinitely.
Completion-order APIs can also be tested, but input order is the contract of this exercise. No particular completion schedule is required.
Submissions must compile, pass the tests and pass clippy -D warnings.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.