rust 123 lines · 3 tabs

Bounded Thread Pool With Backpressure Using Rust Channels

Shared by codesnips Jul 2026
3 tabs
use std::sync::mpsc::{self, SyncSender};
use std::sync::{Arc, Mutex};
use std::thread;

mod worker;
use worker::{Message, Worker};

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

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: SyncSender<Message>,
}

impl ThreadPool {
    pub fn new(size: usize, capacity: usize) -> ThreadPool {
        assert!(size > 0, "pool needs at least one worker");

        let (sender, receiver) = mpsc::sync_channel::<Message>(capacity);
        let receiver = Arc::new(Mutex::new(receiver));

        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,
    {
        // Blocks once the bounded buffer is full: this is the backpressure.
        self.sender
            .send(Message::NewJob(Box::new(f)))
            .expect("all workers have stopped");
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for _ in &self.workers {
            self.sender.send(Message::Terminate).ok();
        }

        for worker in &mut self.workers {
            if let Some(handle) = worker.handle.take() {
                handle.join().expect("worker thread panicked");
            }
        }
    }
}
3 files · rust Explain with highlit

This snippet builds a fixed-size thread pool whose task queue is bounded, so submitting work applies backpressure instead of letting an unbounded backlog grow without limit. The core idea is that a SyncSender with a small buffer will block the producer once the buffer fills, which naturally throttles fast producers to the rate the workers can drain. That property is what makes the pool safe under load: memory stays bounded and slow consumers push back on the caller.

In ThreadPool, the constructor creates a mpsc::sync_channel(capacity) and wraps the Receiver in an Arc<Mutex<..>> so every worker can share the single receive end. Standard-library mpsc is multi-producer but single-consumer, so the Mutex serializes the recv() calls across the worker threads, effectively emulating an MPMC queue. Each spawned worker loops, locks the receiver just long enough to pull one Job, releases the lock, then runs the closure outside the lock so long tasks never block other workers from dequeuing.

Jobs are typed as Box<dyn FnOnce() + Send + 'static>, the canonical boxed-closure shape for a work queue: FnOnce because a job runs exactly once, Send so it can cross the thread boundary, and 'static so it owns its captures. execute sends over the SyncSender; when the buffer is full the send blocks, which is the backpressure in action.

Shutdown is handled by the Message enum in worker.rs. Rather than relying on channel disconnection alone, Drop for ThreadPool sends one Message::Terminate per worker, then joins each JoinHandle. Sending exactly N terminate messages guarantees every worker observes a stop signal even while others are mid-task, and the take() on the optional handle makes the join idempotent. A worker treats a recv() error as an implicit terminate, covering the case where senders are dropped.

The main.rs tab shows the pool draining a batch of jobs and exercising the bound: with capacity two, the eleventh in-flight submission parks the producer until a worker frees a slot. Trade-offs to note: the shared Mutex is a mild contention point under very high throughput, where a dedicated crossbeam MPMC channel would scale better; and panicking jobs currently kill their worker thread, so production code often wraps the call in catch_unwind. This pattern fits CPU-bound or I/O batch work where a stable, bounded number of threads must be reused.


Related snips

Share this code

Here's the card — post it anywhere.

Bounded Thread Pool With Backpressure Using Rust Channels — share card
Link copied