Free Handbook · Every example compiled & verified

Smart Pointers & Concurrency

Box, Deref and Drop, Rc and RefCell, Weak for cycles, then threads, channels, Arc<Mutex<T>>, Send/Sync and scoped threads.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 10 · what you'll be able to do

  • Use Box to put data on the heap and build recursive types like linked lists and trees
  • Predict exactly when a value is dropped, and implement Deref and Drop for your own types
  • Share ownership with Rc, mutate through a shared handle with RefCell, and break reference cycles with Weak
  • Spawn and join threads, move data into them, and pass messages over mpsc channels
  • Share state safely with Arc>, explain what Send and Sync mean, and borrow across threads with thread::scope
01

Box<T>: heap allocation and recursive types

A smart pointer is a struct that acts like a reference but also owns data and does extra work: freeing memory, counting owners, checking borrows at run time. You have already used two of them: String and Vec<T>. This module covers the ones you reach for on purpose, starting with the simplest. Box<T> stores one value on the heap and keeps a pointer to it on the stack. When the box goes out of scope, both are freed.

You need a Box in three situations: a recursive type whose size the compiler cannot compute, a trait object like Box<dyn Shape> (see Module 07), and a large value you want to move without copying all its bytes.

rustmain.rs
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn sum(list: &List) -> i32 {
    match list {
        Cons(v, rest) => v + sum(rest),
        Nil => 0,
    }
}

fn main() {
    let b = Box::new(41);          // an i32 on the heap
    println!("{}", *b + 1);        // * reaches the value inside

    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("{:?}", list);
    println!("sum = {}", sum(&list));
    println!("box size = {} bytes", std::mem::size_of::<Box<List>>());
}
Outputcompiled & run with real Rust
42
Cons(1, Cons(2, Cons(3, Nil)))
sum = 6
box size = 8 bytes

Whatever is inside, a Box is one pointer: 8 bytes on a 64-bit machine. That fixed size is what makes the recursive enum possible. sum(rest) works because &Box<List> turns into &List automatically (the next lesson explains how).

Your turn

Add a function fn len(list: &List) -> usize that counts the Cons cells, and print it for list.

Error you will hit

E0072: a recursive type with infinite size

rust
enum List {
    Cons(i32, List),
    Nil,
}

fn main() {
    let list = List::Cons(1, List::Cons(2, List::Nil));
}
error[E0072]: recursive type `List` has infinite size
 --> main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +
Why the compiler said that

To lay out a List the compiler must know its size. A Cons holds an i32 plus a whole List, which holds an i32 plus a whole List, and so on forever.

The fix

Store the next node behind a pointer. Box<List> has a fixed size (one pointer) no matter how long the list is.

rust
enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
02

Deref and Drop: what makes a pointer smart

Two traits turn a plain struct into a smart pointer. Deref lets *x and method calls reach the value inside. Drop runs cleanup code when the value goes out of scope. Rust also applies deref coercion: when a function wants &str and you pass &String (or &Box<String>, or your own wrapper), the compiler inserts as many deref calls as it needs.

rustmain.rs
use std::ops::Deref;

struct Meters(f64);

struct Wrapper<T> {
    inner: T,
}

impl<T> Deref for Wrapper<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.inner
    }
}

fn shout(s: &str) {
    println!("{}!", s.to_uppercase());
}

fn main() {
    let w = Wrapper { inner: String::from("hello") };
    println!("{}", w.len());      // String::len through Deref
    shout(&w);                    // &Wrapper<String> -> &String -> &str
    let m = Wrapper { inner: Meters(2.5) };
    println!("{}", m.0);
}
Outputcompiled & run with real Rust
5
HELLO!
2.5

Deref coercion is resolved at compile time, so it costs nothing at run time.

Rust frees values deterministically: at the closing brace of the scope that owns them, in the reverse order they were declared. There is no garbage collector deciding "later". You can end a value early with std::mem::drop(x) (in the prelude as plain drop). You may not call x.drop() yourself; the compiler would then run it a second time at the end of the scope.

VisualizeDrop order, step by stepStep 1 / 8
struct Guard {
name: &'static str,
}
impl Drop for Guard {
fn drop(&mut self) {
println!("drop {}", self.name);
}
}
fn main() {
let _a = Guard { name: "a" };
let _b = Guard { name: "b" };
{
let _inner = Guard { name: "inner" };
println!("leaving block");
}
let c = Guard { name: "c" };
drop(c);
println!("end of main");
}
Line 12

_a is created. Nothing is printed on creation.

Variables now
alivea
All 8 steps as a table
StepLineWhat happenedVariables now
112_a is created. Nothing is printed on creation.alive = a
213_b is created after it.alive = a, b
315An inner block creates _inner.alive = a, b, inner
416Still inside the block.alive = a, b, inner
517The block's closing brace ends _inner's scope, so its drop runs now.alive = a, b
619drop(c) moves c into a function that ends immediately, so c is freed early.alive = a, b
720Normal code still runs.alive = a, b
821End of main: remaining locals are dropped in reverse order of declaration, _b first, then _a.alive = (none)
rustmain.rs
struct Conn {
    id: u32,
}

impl Drop for Conn {
    fn drop(&mut self) {
        println!("closing conn {}", self.id);
    }
}

fn main() {
    let pool = vec![Conn { id: 1 }, Conn { id: 2 }];
    let _ = Conn { id: 9 };          // bound to nothing: dropped right away
    let _kept = Conn { id: 3 };      // a real binding: lives to the end
    println!("pool has {}", pool.len());
    println!("main ends");
}
Outputcompiled & run with real Rust
closing conn 9
pool has 2
main ends
closing conn 3
closing conn 1
closing conn 2

Locals drop in reverse (_kept before pool), but the elements inside a Vec drop front to back.

Your turn

Change let _ = to let _unused = and predict where closing conn 9 moves to.

<code>let _ = </code> is not the same as <code>let _name = </code>
The bare _ pattern does not bind, so the value is dropped at the end of that statement. With a lock guard this is a classic bug: let _ = mutex.lock().unwrap(); takes the lock and releases it on the same line. Use let _guard = ... when you want the value to live to the end of the scope.
03

Rc<T>: several owners, one value

Ownership says every value has exactly one owner. Some data does not fit that: a config read by several components, a node in a graph with two incoming edges. Rc<T> (reference counted) keeps the value on the heap along with a count of owners. Rc::clone adds an owner by bumping the count; it does not copy the data. When the last Rc is dropped and the count reaches zero, the value is freed.

rustmain.rs
use std::rc::Rc;

#[derive(Debug)]
struct Config {
    name: String,
}

fn main() {
    let cfg = Rc::new(Config { name: String::from("prod") });
    println!("count after new = {}", Rc::strong_count(&cfg));

    let api = Rc::clone(&cfg);            // cheap: bumps a counter
    {
        let worker = Rc::clone(&cfg);
        println!("count inside = {}", Rc::strong_count(&cfg));
        println!("worker sees {}", worker.name);
    }
    println!("count after block = {}", Rc::strong_count(&cfg));
    println!("same allocation: {}", Rc::ptr_eq(&cfg, &api));
    drop(cfg);
    println!("api still has {:?}, count = {}", api, Rc::strong_count(&api));
}
Outputcompiled & run with real Rust
count after new = 1
count inside = 3
worker sees prod
count after block = 2
same allocation: true
api still has Config { name: "prod" }, count = 1

Writing Rc::clone(&cfg) rather than cfg.clone() is the convention: it tells the reader this is a cheap count bump, not a deep copy.

Your turn

Add a third handle before the block and predict every count the program prints.

Rc has two limits. It is single-threaded: its counter is not atomic, so the compiler will not let it cross into another thread (that is what Arc is for, later in this module). And it only hands out shared references, because several owners mutating the same value would break the borrowing rules.

Error you will hit

E0594: assigning through an Rc

rust
use std::rc::Rc;

fn main() {
    let score = Rc::new(10);
    let other = Rc::clone(&score);
    *score += 5;
    println!("{} {}", score, other);
}
error[E0594]: cannot assign to data in an `Rc`
 --> main.rs:6:5
  |
6 |     *score += 5;
  |     ^^^^^^^^^^^ cannot assign
  |
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<i32>`
Why the compiler said that

Rc implements Deref but not DerefMut. If score could change the value, other would see it change underneath a shared reference, which is exactly what the borrow checker exists to prevent.

The fix

Put a type with interior mutability inside the Rc: Rc<Cell<i32>> for small Copy values, Rc<RefCell<T>> for anything else (next lesson).

rust
use std::cell::Cell;
use std::rc::Rc;

fn main() {
    let score = Rc::new(Cell::new(10));
    let other = Rc::clone(&score);
    score.set(score.get() + 5);
    println!("{} {}", score.get(), other.get());
}
04

RefCell<T> and interior mutability

Interior mutability means changing data through a shared reference &T. It sounds like it breaks the rules, but it only moves the check. RefCell<T> enforces "many readers or one writer" at run time: borrow() hands out a shared guard, borrow_mut() an exclusive one, and breaking the rule panics instead of failing to compile. Cell<T> is the lighter version for Copy values: get and set copy in and out, so there is no borrow to track.

rustmain.rs
use std::cell::{Cell, RefCell};

struct Counter {
    hits: Cell<u32>,
    log: RefCell<Vec<String>>,
}

impl Counter {
    fn record(&self, msg: &str) {        // &self, yet it changes state
        self.hits.set(self.hits.get() + 1);
        self.log.borrow_mut().push(msg.to_string());
    }
}

fn main() {
    let c = Counter { hits: Cell::new(0), log: RefCell::new(Vec::new()) };
    c.record("login");
    c.record("view");
    println!("hits = {}", c.hits.get());
    println!("log = {:?}", c.log.borrow());

    let r1 = c.log.borrow();
    let r2 = c.log.borrow();            // many readers: fine
    println!("{} {}", r1.len(), r2[0]);
    drop(r1);
    drop(r2);
    println!("{}", c.log.try_borrow_mut().is_ok());
}
Outputcompiled & run with real Rust
hits = 2
log = ["login", "view"]
2 login
true

try_borrow_mut returns a Result instead of panicking. It is Ok here because both readers were dropped first.

Error you will hit

Runtime panic: RefCell already borrowed (BorrowMutError)

rust
use std::cell::RefCell;

fn main() {
    let log = RefCell::new(vec![String::from("start")]);
    let first = log.borrow();
    log.borrow_mut().push(String::from("next"));
    println!("{}", first[0]);
}
thread 'main' (13291973) panicked at main.rs:6:9:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Why the compiler said that

This compiles, because RefCell defers the borrow check to run time. At line 6 the shared guard first is still alive, so the request for a mutable borrow breaks "one writer, no readers" and borrow_mut panics. Without RefCell the same code would be a compile-time E0502.

The fix

Keep guards short-lived: finish reading (or clone what you need) before borrowing mutably. Scoping the read in a block or calling drop(first) both work. If you cannot be sure, use try_borrow_mut and handle the Err.

rust
use std::cell::RefCell;

fn main() {
    let log = RefCell::new(vec![String::from("start")]);
    let first = log.borrow()[0].clone();
    log.borrow_mut().push(String::from("next"));
    println!("{}", first);
}

Rc<RefCell<T>>: shared and mutable

Combine the two and you get a value with several owners, any of whom can change it: Rc supplies the owners, RefCell the mutation. This is the standard single-threaded pattern for shared mutable state such as a graph node, a UI model, or a mock object in tests.

rustmain.rs
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Account {
    owner: String,
    balance: i64,
}

fn main() {
    let shared = Rc::new(RefCell::new(Account { owner: String::from("ana"), balance: 100 }));

    let mobile = Rc::clone(&shared);
    let web = Rc::clone(&shared);

    mobile.borrow_mut().balance -= 30;
    web.borrow_mut().balance += 5;

    let acct = shared.borrow();
    println!("{} has {}", acct.owner, acct.balance);
    println!("owners = {}", Rc::strong_count(&shared));
}
Outputcompiled & run with real Rust
ana has 75
owners = 3

Each borrow_mut() guard lives only until the end of its statement, so the two writes never overlap.

Your turn

Hold let g = mobile.borrow_mut(); in a variable and then call web.borrow_mut(). Predict the panic before running it.

In real code
Reaching for Rc<RefCell<T>> everywhere is a sign of fighting the borrow checker. Experienced Rust developers first try plain ownership, passing &mut down the call stack, or storing items in a Vec and referring to them by index. Use shared mutable state when the data really is shared.
05

Weak<T>: breaking reference cycles

Reference counting has one blind spot. If A holds an Rc to B and B holds an Rc to A, neither count can ever reach zero and both leak. Safe Rust does not prevent leaks, so this is on you. The fix is Weak<T>: a non-owning handle that does not keep the value alive. Make one with Rc::downgrade(&rc), and turn it back into an Option<Rc<T>> with upgrade(), which returns None once the value is gone.

The rule of thumb for trees: parents own children (Rc), children point back at their parent with Weak.

rustmain.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        name: String::from("leaf"),
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade().map(|p| p.name.clone()));

    {
        let branch = Rc::new(Node {
            name: String::from("branch"),
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });
        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!("leaf parent = {:?}", leaf.parent.borrow().upgrade().map(|p| p.name.clone()));
        println!("branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch));
        println!("branch has {} child", branch.children.borrow().len());
    }

    // branch is gone: the Weak did not keep it alive
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade().map(|p| p.name.clone()));
    println!("leaf strong = {}", Rc::strong_count(&leaf));
}
Outputcompiled & run with real Rust
leaf parent = None
leaf parent = Some("branch")
branch strong = 1, weak = 1
branch has 1 child
leaf parent = None
leaf strong = 1

Only strong counts keep a value alive. When branch left its block its strong count hit zero, it was freed (dropping its Rc to leaf, so leaf's count fell back to 1), and the leaf's Weak now upgrades to None.

Your turn

Change parent to RefCell<Option<Rc<Node>>>. It still runs, but add a Drop impl that prints the name and notice that neither node is ever dropped: that is the leak.

Which smart pointer to reach for. Rc + RefCell for single-threaded shared state, Arc + Mutex for the multi-threaded version.
TypeOwnersMutationCheckedThreads
Box<T>OneYes, if you own it mutablyCompile timeYes, if T is Send
Rc<T>ManyNoCompile timeNo
Weak<T>None (observer)Noupgrade() may be NoneNo
Cell<T>OneYes, via get/set (Copy values)Nothing to checkNo
RefCell<T>OneYes, via borrow_mutRun time (panics)No
Arc<T>ManyNoCompile timeYes
Mutex<T> / RwLock<T>OneYes, via lock / writeRun time (blocks)Yes
06

Threads: spawn, join and move closures

std::thread::spawn starts an operating-system thread that runs a closure. It returns a JoinHandle; calling join() waits for the thread to finish and gives you its return value as a Result (an Err if the thread panicked). If main returns first, the whole process exits and unfinished threads are killed mid-work, so always join the threads whose work you need.

rustmain.rs
use std::thread;

fn main() {
    let mut handles = Vec::new();
    for id in 1..=3 {
        handles.push(thread::spawn(move || {
            let sum: u64 = (1..=id * 1000).sum();
            (id, sum)                       // the thread's return value
        }));
    }

    // join waits for each thread and hands back its result, in spawn order
    for h in handles {
        let (id, sum) = h.join().unwrap();
        println!("thread {} summed to {}", id, sum);
    }
    println!("all done");
}
Outputcompiled & run with real Rust
thread 1 summed to 500500
thread 2 summed to 2001000
thread 3 summed to 4501500
all done

The threads really do run at the same time and may finish in any order. The output is still predictable because main does all the printing, joining the handles in the order it spawned them.

Your turn

Move the println! inside the thread closure and run it several times. The lines can come out in a different order each run, which is why examples in this handbook print after joining.

The closure passed to spawn must be 'static: it cannot hold references to local variables, because the new thread might outlive the function that created them. The fix is the move keyword you met in Module 09, which makes the closure own its captures.

Error you will hit

E0373: a thread closure that borrows a local

rust
use std::thread;

fn main() {
    let name = String::from("worker");
    let handle = thread::spawn(|| {
        println!("hello from {}", name);
    });
    handle.join().unwrap();
}
error[E0373]: closure may outlive the current function, but it borrows `name`, which is owned by the current function
 --> main.rs:5:32
  |
5 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `name`
6 |         println!("hello from {}", name);
  |                                   ---- `name` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> main.rs:5:18
  |
5 |       let handle = thread::spawn(|| {
  |  __________________^
6 | |         println!("hello from {}", name);
7 | |     });
  | |______^
help: to force the closure to take ownership of `name` (and any other referenced variables), use the `move` keyword
  |
5 |     let handle = thread::spawn(move || {
  |                                ++++
Why the compiler said that

The closure only reads name, so it captures a reference. The compiler cannot prove the thread ends before main drops name (the join on line 8 is not something the type system tracks), so the borrow might dangle.

The fix

Add move so the thread owns name. If you still need the value in main, clone it first and move the clone, or use scoped threads (the last lesson of this module), which are allowed to borrow.

rust
use std::thread;

fn main() {
    let name = String::from("worker");
    let handle = thread::spawn(move || {
        println!("hello from {}", name);
    });
    handle.join().unwrap();
}
07

Message passing with mpsc channels

One way to avoid shared state is to not share at all: threads send each other values. std::sync::mpsc::channel() returns a sender tx and a receiver rx. "mpsc" stands for multiple producer, single consumer: clone tx as often as you like, but there is one rx. send moves the value into the channel, so the sending thread can no longer touch it: ownership travels with the message.

rustmain.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    let producer = thread::spawn(move || {
        for job in ["parse", "validate", "store"] {
            tx.send(job.to_string()).unwrap();
        }
        // tx is dropped here, which closes the channel
    });

    for msg in rx {                      // ends when every sender is gone
        println!("got {}", msg);
    }
    producer.join().unwrap();
}
Outputcompiled & run with real Rust
got parse
got validate
got store

With one producer, messages arrive in the order they were sent. Iterating rx calls recv() until the channel is closed.

rustmain.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    for worker in 1..=3 {
        let tx = tx.clone();             // one sender per thread
        thread::spawn(move || {
            for n in 0..2 {
                tx.send(format!("w{}-{}", worker, n)).unwrap();
            }
        });
    }
    drop(tx);                            // drop the original, or rx waits forever

    let mut results: Vec<String> = rx.iter().collect();
    results.sort();                      // arrival order varies; sort before printing
    println!("{} messages", results.len());
    println!("{:?}", results);
}
Outputcompiled & run with real Rust
6 messages
["w1-0", "w1-1", "w2-0", "w2-1", "w3-0", "w3-1"]

With several producers the interleaving changes from run to run. Collecting and sorting gives a stable result.

Your turn

Delete the drop(tx); line and run it. The program hangs: rx.iter() keeps waiting because main still holds a sender.

Bounded channels apply backpressure
mpsc::sync_channel(n) creates a channel that holds at most n messages; send blocks when it is full. Use it when a fast producer could otherwise fill memory faster than the consumer drains it.
08

Shared state: Arc<Mutex<T>> and RwLock

When threads really must share one value, you need the thread-safe versions of the two pieces from earlier. Arc<T> (atomically reference counted) is Rc with an atomic counter, safe to clone across threads. Mutex<T> plays the role of RefCell: lock() waits until no other thread holds the lock, then returns a MutexGuard that derefs to the data and unlocks automatically when dropped. The data lives inside the mutex, so you cannot touch it without locking. That is the difference from mutexes in C or Java, where forgetting to lock is a silent bug.

rustmain.rs
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let total = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();

    for i in 1..=4 {
        let total = Arc::clone(&total);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let mut guard = total.lock().unwrap();
                *guard += i;
            }                            // guard dropped: lock released
        }));
    }
    for h in handles {
        h.join().unwrap();
    }

    println!("total = {}", *total.lock().unwrap());
    println!("owners left = {}", Arc::strong_count(&total));
}
Outputcompiled & run with real Rust
total = 10000
owners left = 1

1000 × (1 + 2 + 3 + 4) = 10000 every time: no update is lost, because only one thread at a time can hold the guard. lock() returns a Result that is Err only if another thread panicked while holding the lock ("poisoning").

Error you will hit

E0382: moving a Mutex into the first thread

rust
use std::sync::Mutex;
use std::thread;

fn main() {
    let total = Mutex::new(0);
    let mut handles = Vec::new();
    for i in 1..=3 {
        handles.push(thread::spawn(move || {
            *total.lock().unwrap() += i;
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("{}", total.lock().unwrap());
}
error[E0382]: borrow of moved value: `total`
  --> main.rs:15:20
   |
 5 |     let total = Mutex::new(0);
   |         ----- move occurs because `total` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
 6 |     let mut handles = Vec::new();
 7 |     for i in 1..=3 {
   |     -------------- inside of this loop
 8 |         handles.push(thread::spawn(move || {
   |                                    ------- value moved into closure here, in previous iteration of loop
...
15 |     println!("{}", total.lock().unwrap());
   |                    ^^^^^ value borrowed here after move
Why the compiler said that

A Mutex has one owner. The first loop iteration moves it into the first thread, so there is nothing left for the second thread or for main.

The fix

Wrap it in an Arc and move a Arc::clone into each thread; every clone points at the same mutex.

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

fn main() {
    let total = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();
    for i in 1..=3 {
        let total = Arc::clone(&total);
        handles.push(thread::spawn(move || {
            *total.lock().unwrap() += i;
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("{}", total.lock().unwrap());
}

When reads far outnumber writes, RwLock<T> allows any number of readers at once (read()) or one writer (write()), the thread-safe twin of RefCell's rule. For a single counter or flag, the atomic types in std::sync::atomic (AtomicUsize, AtomicBool) are faster still, with no lock at all.

rustmain.rs
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let settings = Arc::new(RwLock::new(vec![String::from("dark-mode")]));

    let readers: Vec<_> = (0..3)
        .map(|_| {
            let s = Arc::clone(&settings);
            thread::spawn(move || s.read().unwrap().len())
        })
        .collect();
    let counts: Vec<usize> = readers.into_iter().map(|h| h.join().unwrap()).collect();
    println!("{:?}", counts);

    settings.write().unwrap().push(String::from("beta"));
    println!("{:?}", settings.read().unwrap());
}
Outputcompiled & run with real Rust
[1, 1, 1]
["dark-mode", "beta"]
Your turn

Replace the RwLock with a Mutex. The program still works; what do you lose?

Rust prevents data races, not deadlocks
If thread 1 locks A then B while thread 2 locks B then A, both wait forever and the compiler cannot see it coming. Always take multiple locks in the same order, and keep guards alive for as short a time as possible.
09

Send, Sync and scoped threads

How does the compiler know Rc may not cross threads but Arc may? Two marker traits, with no methods, that the compiler implements automatically for types made of safe parts:

  • Send: ownership of this value can be moved to another thread. Almost everything is Send; Rc is not, because two threads bumping its plain counter at once could corrupt it.
  • Sync: a &T can be shared between threads. T is Sync exactly when &T is Send. RefCell and Cell are Send but not Sync; Mutex<T> is Sync, which is why Arc<Mutex<T>> works.

thread::spawn requires its closure to be Send + 'static. Put a non-Send type in the closure and the program does not compile. That is the famous "fearless concurrency" claim: a data race is a type error, not a 3 a.m. bug report.

Error you will hit

E0277: `Rc` cannot be sent between threads safely

rust
use std::rc::Rc;
use std::thread;

fn main() {
    let counter = Rc::new(5);
    let c = Rc::clone(&counter);
    let handle = thread::spawn(move || {
        println!("{}", c);
    });
    handle.join().unwrap();
}
error[E0277]: `Rc<i32>` cannot be sent between threads safely
   --> main.rs:7:32
    |
  7 |       let handle = thread::spawn(move || {
    |                    ------------- ^------
    |                    |             |
    |  __________________|_____________within this `{[email protected]:7:32: 7:39}`
    | |                  |
    | |                  required by a bound introduced by this call
  8 | |         println!("{}", c);
  9 | |     });
    | |_____^ `Rc<i32>` cannot be sent between threads safely
    |
    = help: within `{[email protected]:7:32: 7:39}`, the trait `Send` is not implemented for `Rc<i32>`
note: required because it's used within this closure
   --> main.rs:7:32
    |
  7 |     let handle = thread::spawn(move || {
    |                                ^^^^^^^
Why the compiler said that

The closure captures c, an Rc. If it ran on another thread while main still held counter, both threads could update the non-atomic reference count at the same moment. Rc is deliberately not Send, so the closure is not Send either, and spawn rejects it.

The fix

Swap Rc for Arc. The API is the same; only the counter is atomic (slightly slower, which is why Rc exists at all).

rust
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(5);
    let c = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        println!("{}", c);
    });
    handle.join().unwrap();
}

Scoped threads: borrow instead of move

thread::scope (stable since Rust 1.63) creates a scope whose threads are all joined before it returns. Because the compiler knows they end in time, those threads may borrow local variables: no 'static, no Arc, no clones. For "split this work across cores and wait for the result" it is usually the cleanest tool.

rustmain.rs
use std::thread;

fn main() {
    let words = vec!["alpha", "beta", "gamma", "delta"];
    let mut lengths = vec![0; words.len()];

    thread::scope(|s| {
        for (word, slot) in words.iter().zip(lengths.iter_mut()) {
            s.spawn(move || {
                *slot = word.len();      // writes into main's Vec, no Arc needed
            });
        }
    }); // every scoped thread is joined here

    println!("{:?}", words);             // still usable: only borrowed
    println!("{:?}", lengths);
}
Outputcompiled & run with real Rust
["alpha", "beta", "gamma", "delta"]
[5, 4, 5, 5]

Each thread gets a &mut to a different slot from iter_mut(), so the borrow checker can prove no two threads write the same element. move here moves the references into each closure, not the vectors.

Your turn

Change the threads to all push onto one shared Vec through &mut. The compiler refuses; wrap the Vec in a Mutex (still no Arc needed inside a scope).

Quick check

You need a counter that four threads increment. Which type fits?

Smart pointer
A struct that behaves like a reference (through Deref) but also owns data and runs extra logic, such as freeing or counting.
Box<T>
A single-owner pointer to a heap value. Fixed size, so it makes recursive types and trait objects possible.
Deref coercion
The compiler automatically converting &Wrapper to &Target (for example &String to &str) to fit a function signature.
Drop
The trait whose drop method runs when a value goes out of scope. Locals drop in reverse declaration order.
Rc<T> / Arc<T>
Reference-counted shared ownership. Arc uses an atomic counter so it can cross threads.
Interior mutability
Mutating data through a shared reference, with the borrow rule checked at run time (Cell, RefCell, Mutex).
Weak<T>
A non-owning handle to an Rc value. upgrade() returns None once the value is dropped. Used to break cycles.
Mutex<T>
A lock that owns its data. lock() returns a guard that unlocks when dropped.
Send / Sync
Marker traits: a Send value may move to another thread; a Sync value may be shared by reference between threads.
Scoped thread
A thread started with thread::scope that is guaranteed to be joined before the scope ends, so it may borrow locals.
Mid-levelWhat is the difference between Rc> and Arc>, and why can you not use the first across threads?

Both give you shared ownership plus mutation. Rc counts owners with a plain integer and RefCell tracks borrows with a plain flag and panics on a conflict: cheap, but racy if two threads touched them at once, so Rc is not Send and RefCell is not Sync. Arc uses an atomic counter and Mutex makes other threads wait for the lock instead of panicking. The compiler enforces the choice through the Send + 'static bound on thread::spawn.

What they are really testing: Whether you understand that Send/Sync are what turn thread-safety mistakes into compile errors, and the cost trade-off between the single-threaded and thread-safe types.

Frequently asked questions

When should I use Box, Rc or Arc in Rust?
Box when a value has one owner but must live on the heap (recursive types, trait objects, large values). Rc when several parts of a single-threaded program need to own the same value. Arc when the owners are on different threads. Add RefCell (single-threaded) or Mutex/RwLock (multi-threaded) inside when the shared value must change.
Can Rust code still leak memory?
Yes. Leaks are memory-safe, so the compiler does not forbid them. The usual cause is a reference cycle of Rc or Arc values that keep each other alive; break it by making one direction a Weak pointer. Box::leak and mem::forget leak on purpose.
Does Rust prevent all concurrency bugs?
It prevents data races at compile time through the ownership rules and the Send and Sync traits. It does not prevent deadlocks, lock contention or logic races such as check-then-act across two separate lock calls, so you still need to design lock order and scope carefully.

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.