How to implement a thread pool

Implement a Rust thread pool by adding the rayon crate and using par_iter() to parallelize collection processing.

When one owner isn't enough

Rust's ownership rule says every value has exactly one owner. The owner is responsible for cleaning the value up when its scope ends. That rule keeps the language safe and fast. It also occasionally gets in your way.

Picture a graph. Several nodes need to know about a shared list of categories. Or a parser where multiple syntax tree branches all reference the same source string. Or a UI where a piece of state is read by three different views. There's no single "main" owner. Several places need to be able to read the same data, and the data should stay alive as long as anyone is still using it.

That's what Rc<T> is for. The name stands for "reference counted." Think of it like a library card on the inside cover of a book: every time someone borrows the book, the librarian writes a tally mark. When someone returns the book, the librarian crosses one out. When the last mark gets crossed off, the book goes back on the shelf and the storage is reclaimed.

How it actually works

Rc::new(value) puts your value on the heap, alongside a counter that starts at one. When you call Rc::clone, you don't copy the value. You bump the counter and hand back another Rc pointing at the same heap allocation. Each Rc that goes out of scope decrements the counter. When the counter hits zero, the value gets dropped and the memory is freed.

use std::rc::Rc;

fn main() {
    // The String goes on the heap. The Rc wrapper holds a pointer plus a counter.
    let data = Rc::new(String::from("Shared payload"));

    // clone() bumps the reference count. It does NOT copy the String.
    let data2 = Rc::clone(&data);

    // Both Rc instances point to the same heap allocation.
    // The counter is now 2.
    println!("data: {}", data);
    println!("data2: {}", data2);

    // Dropping data2 decrements the counter to 1.
    drop(data2);

    // Dropping data decrements the counter to 0.
    // The String is dropped and memory is freed.
    drop(data);
}

The convention in Rust is to write Rc::clone(&data) instead of data.clone(). Both compile and both do the exact same thing. The explicit form signals to the reader that you are cloning the reference, not the underlying data. A reader seeing data.clone() might assume a deep copy is happening. The explicit call removes that ambiguity.

When you need mutation

Rc<T> gives you shared ownership, but it only gives you shared reading. If you try to mutate the value through an Rc, the compiler stops you. You can't have multiple owners mutating the same data at the same time; that leads to data races.

Rust solves this with interior mutability. You wrap the value in a RefCell<T> inside the Rc. The RefCell enforces borrowing rules at runtime instead of compile time. You can have multiple Rc<RefCell<T>> handles, but only one can hold a mutable borrow at any given moment. If you violate the rules, the program panics.

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let node = Rc::new(Node {
        value: 1,
        children: RefCell::new(vec![]),
    });

    // Clone the Rc to create a second handle.
    let child = Rc::new(Node {
        value: 2,
        children: RefCell::new(vec![]),
    });

    // borrow_mut() checks at runtime that no one else is holding a reference.
    // If the check fails, the program panics.
    node.children.borrow_mut().push(Rc::clone(&child));

    println!("{:?}", node);
}

This pattern is the standard way to build graphs, trees, and other cyclic data structures in Rust. The RefCell lets you mutate the children list even though the Node is behind an Rc. The runtime check ensures you never have two mutable references to the same node.

The cost of reference counting

Reference counting has a price. Every clone and every drop requires an atomic operation to update the counter. On a single thread, these operations are cheap but not free. They add overhead to every allocation and deallocation. In tight loops where you create and drop thousands of Rc instances, this overhead can show up in benchmarks.

The bigger cost appears when you introduce threads. Rc is not thread-safe. The counter update is not atomic across threads. If you try to move an Rc into another thread, the compiler rejects you with E0277 (trait bound not satisfied). Rc does not implement Send.

For multi-threaded code, you use Arc<T> (Atomic Reference Counted). Arc uses atomic operations for the counter, making it safe to share across threads. Atomic operations are more expensive than the non-atomic operations in Rc. If your code is single-threaded, stick with Rc. If you need thread safety, switch to Arc.

Cycles and memory leaks

Reference counting has a fatal flaw: it cannot handle cycles. If two Rc instances point to each other, the counters never reach zero. The memory leaks.

use std::rc::Rc;

#[derive(Debug)]
struct Parent {
    children: Vec<Rc<Child>>,
}

#[derive(Debug)]
struct Child {
    // This creates a cycle. Parent holds Child, Child holds Parent.
    parent: Rc<Parent>,
}

fn main() {
    let parent = Rc::new(Parent { children: vec![] });
    let child = Rc::new(Child { parent: Rc::clone(&parent) });

    // This push creates the cycle.
    parent.children.push(Rc::clone(&child));

    // When main ends, parent and child go out of scope.
    // The counters decrement, but they never hit zero.
    // The memory is never freed.
}

Rust breaks cycles with Weak<T>. A Weak reference points to the same allocation as an Rc, but it doesn't increment the strong counter. It only increments a weak counter. The value is dropped when the strong counter hits zero, even if weak references still exist. After the value is dropped, Weak references become invalid. You must upgrade them to an Option<Rc<T>> before using them.

use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Parent {
    children: Vec<Rc<Child>>,
}

#[derive(Debug)]
struct Child {
    // Weak breaks the cycle. It doesn't keep the parent alive.
    parent: Weak<Parent>,
}

fn main() {
    let parent = Rc::new(Parent { children: vec![] });
    let child = Rc::new(Child {
        parent: Rc::downgrade(&parent),
    });

    parent.children.push(Rc::clone(&child));

    // Upgrade the weak reference to check if the parent still exists.
    if let Some(parent_ref) = child.parent.upgrade() {
        println!("Parent is alive: {:?}", parent_ref);
    }
}

The convention is to use Weak for back-references in trees and graphs. The parent owns the children via Rc. The children point back to the parent via Weak. This ensures the tree drops cleanly when the root is dropped.

Decision matrix

Use Rc<T> when you have multiple owners in a single-threaded context and the data is read-only. Use Rc<RefCell<T>> when you need shared ownership with interior mutability on a single thread. Use Arc<T> when you need to share data across multiple threads. Use Arc<Mutex<T>> or Arc<RwLock<T>> when you need shared ownership with interior mutability across threads. Use Weak<T> to break reference cycles in graphs and trees. Reach for plain ownership or references when the data has a single clear owner; reference counting adds overhead that isn't needed for simple cases.

Where to go next