---
title: "26.3 Building the thread pool"
description: "Implementing the pool: workers, a channel to send jobs, an Arc<Mutex> shared receiver, and the Box<dyn FnOnce + Send> job type that ties the course together."
url: "https://learnrust.net/chapter-26/building-the-thread-pool/"
last_updated: "2026-06-13"
---

# 26.3 Building the thread pool

This is the centerpiece. We have the interface from lesson [26.2](https://learnrust.net/chapter-26/from-single-to-multithreaded/), `ThreadPool::new(4)` and `pool.execute(closure)`, and now we build it. This single type pulls together more of the course than anything else you'll write: threads and channels and `Arc<Mutex>` from chapter 22, the closure traits from chapter 19, `Box` from chapter 21, all in one small module. We'll build it compiler-driven: write the type, let the errors guide each next move.

## The Job type: where four chapters meet

Start with the hardest question: what *is* a unit of work? `execute` receives a closure and must store it, send it to another thread, and have a worker run it exactly once. Translate each of those requirements into a trait bound, and you get one of the most instructive type signatures in the course:

```rust
type Job = Box<dyn FnOnce() + Send + 'static>;
```

Read it piece by piece, because every piece is a chapter:

- **`FnOnce()`** (chapter 19): the closure takes no arguments and is called *once*, a worker runs each job a single time, so `FnOnce` is the right trait of the three.
- **`dyn FnOnce()`** (chapter 16): each closure has its own anonymous type, so we can't name a concrete one; `dyn` makes it a trait object, "some closure with this signature."
- **`Box<...>`** (chapter 21): a trait object is unsized (closures vary in size), so it must live behind a pointer; `Box` puts it on the heap with a known-size handle.
- **`Send`** (chapter 22): the job travels from `main` to a worker thread, so it must be safe to send across threads.
- **`'static`** (chapter 17): the job may live as long as the worker, so it can't borrow anything short-lived.

That one line is a summary of the second half of the course. When you can read `Box<dyn FnOnce() + Send + 'static>` and explain why each part is there, you understand Rust's type system. We needed all of it just to say "a closure I can store, ship to another thread, and run once."

## The channel and the workers

Now the delivery mechanism. `main` produces jobs; workers consume them. That's a producer/consumer relationship, which is exactly what channels are for (lesson [22.2](https://learnrust.net/chapter-22/message-passing-channels/)). The pool holds the *sending* end; the workers share the *receiving* end. But a channel has *one* receiver, and we have *several* workers all needing to pull from it. So the receiver must be shared among threads and accessed one-at-a-time, which is precisely `Arc<Mutex<T>>` (lesson [22.3](https://learnrust.net/chapter-22/shared-state-mutex-and-arc/)): `Arc` to share the receiver across workers, `Mutex` so only one worker takes a job at a time.

```rust
use std::sync::{mpsc, Arc, Mutex};
use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));   // share one receiver among workers

        let mut workers = Vec::with_capacity(size);
        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);
        self.sender.send(job).unwrap();
    }
}
```

Trace it. `new` asserts a positive size, creates one channel, and wraps the receiver in `Arc<Mutex<>>` so it can be shared. It then builds `size` workers, handing each a *clone* of the shared receiver (`Arc::clone`, the cheap handle from chapter 21). The pool keeps the `sender`. `execute` is short and complete: it boxes the incoming closure into a `Job` and `send`s it down the channel. Whichever worker grabs it next will run it. Notice `execute`'s bounds, `F: FnOnce() + Send + 'static`, are the same constraints that the `Job` type spelled out; the generic parameter on `execute` and the `dyn` in `Job` are two views of the same requirement.

## The worker loop

A `Worker` owns a thread that loops forever: lock the receiver, wait for a job, run it, repeat.

```rust
struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver
                .lock()              // acquire the mutex
                .unwrap()
                .recv();             // wait for a job from the channel

            match job {
                Ok(job) => {
                    println!("Worker {id} got a job; executing.");
                    job();           // run the closure
                }
                Err(_) => {
                    println!("Worker {id} disconnected; shutting down.");
                    break;
                }
            }
        });

        Worker { id, thread }
    }
}
```

Each worker `spawn`s a thread (with `move`, so it owns its `id` and its receiver handle, lesson [22.1](https://learnrust.net/chapter-22/threads/)). Inside, it loops: `receiver.lock().unwrap().recv()` locks the shared mutex, then blocks on `recv()` waiting for a job. Crucially, the lock is held only momentarily, the `MutexGuard` from `lock()` is a temporary that drops right after `recv()` returns, *before* `job()` runs, so one worker running a long job doesn't keep the others from grabbing the next one. When `recv` returns `Ok(job)`, the worker runs the closure; when it returns `Err` (the channel closed, all senders gone), the worker breaks out of its loop and ends, which is how the next lesson shuts the pool down cleanly.

## Wiring it into the server

Drop the pool into `main` from lesson 26.1, and the server is now multithreaded:

```rust
fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();
        pool.execute(|| {
            handle_connection(stream);
        });
    }
}
```

That's the interface we designed, now backed by a real pool. Re-run the experiment from lesson 26.1: request `/sleep` (the 5-second route) in one tab and `/` in another. This time the fast page loads *immediately*, because one of the four workers handles the slow request while the others stay free. The server now serves up to four connections concurrently, and never more than four threads exist no matter how many requests flood in. Bounded concurrency, achieved.

> **The whole course in one module**
>
> Step back and count what this small pool used: a channel to pass work (chapter 22), `Arc<Mutex>` to share the receiver safely across workers (chapters 21 and 22), `thread::spawn` with `move` closures (chapters 19 and 22), `Box<dyn FnOnce() + Send + 'static>` combining trait objects, closures, the heap, `Send`, and lifetimes (chapters 16, 19, 21, 22, 17), and structs, generics, and error handling holding it together (chapters 10, 15, 12). No chapter was wasted. A thread pool is a genuinely advanced concurrent data structure, and you just built one from parts you understand individually. That's what the course was for.

## Quiz time

**Question #1**

Explain why a job is typed `Box<dyn FnOnce() + Send + 'static>`, one reason per piece.

<details class="solution">
<summary>Show solution</summary>

`FnOnce()` because a worker runs each job exactly once (chapter 19). `dyn` because each closure has a unique anonymous type that can't be named, so it's a trait object (chapter 16). `Box` because a trait object is unsized and must live behind a pointer on the heap (chapter 21). `Send` because the job is sent from `main` to a worker thread and must be safe to cross threads (chapter 22). `'static` because the job may live as long as the worker, so it can't borrow anything short-lived (chapter 17).

</details>

**Question #2**

Why is the channel's receiver wrapped in `Arc<Mutex<...>>`?

<details class="solution">
<summary>Show solution</summary>

A channel has a single receiver, but multiple workers all need to pull jobs from it. `Arc` lets the one receiver be *shared* (owned) across all the worker threads (each gets a cheap clone of the handle), and `Mutex` ensures only *one* worker takes a job at a time, so two workers can't grab the same job or corrupt the receiver. It's the chapter-22 `Arc<Mutex<T>>` pattern: shared ownership plus safe, exclusive access.

</details>

**Question #3**

Why does the worker hold the mutex lock only briefly, and why does that matter?

<details class="solution">
<summary>Show solution</summary>

The worker locks the receiver just long enough to call `recv()` and get a job; the `MutexGuard` is a temporary that drops right after, *before* the job runs, so the lock is released while `job()` executes. This matters because if the worker held the lock for the whole duration of running the job, no other worker could acquire it to take the next job, serializing everything and defeating the pool. Releasing the lock before running the job lets the other workers grab jobs concurrently.

</details>

The server handles many connections at once. But it never stops cleanly, kill it and any in-progress requests die mid-flight, and the workers are never properly joined. The final lesson ([26.4](https://learnrust.net/chapter-26/graceful-shutdown/)) adds graceful shutdown with a `Drop` implementation (chapter 21), and closes the course.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
