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
123456789101112131415161718192021222324
#[derive(Debug)]enumList{Cons(i32,Box<List>),Nil,}useList::{Cons,Nil};fnsum(list:&List)-> i32 {match list {Cons(v, rest)=> v +sum(rest),Nil=>0,}}fnmain(){let b =Box::new(41);// an i32 on the heapprintln!("{}",*b +1);// * reaches the value insidelet 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>>());}
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
12345678
enumList{Cons(i32,List),Nil,}fnmain(){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
12345678
enumList{Cons(i32,Box<List>),Nil,}fnmain(){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
1234567891011121314151617181920212223242526
use std::ops::Deref;structMeters(f64);structWrapper<T>{
inner: T,}impl<T>DerefforWrapper<T>{typeTarget= T;fnderef(&self)->&T {&self.inner
}}fnshout(s:&str){println!("{}!", s.to_uppercase());}fnmain(){let w =Wrapper{ inner:String::from("hello")};println!("{}", w.len());// String::len through Derefshout(&w);// &Wrapper<String> -> &String -> &strlet 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
1struct Guard {
2 name:&'static str,
3}
4
5impl Drop for Guard {
6 fn drop(&mut self){
7 println!("drop {}", self.name);
8}
9}
10
11fn main(){
12 let _a = Guard { name:"a"};
13 let _b = Guard { name:"b"};
14{
15 let _inner = Guard { name:"inner"};
16 println!("leaving block");
17}
18 let c = Guard { name:"c"};
19 drop(c);
20 println!("end of main");
21}
Line 12
_a is created. Nothing is printed on creation.
Variables now
alive
a
All 8 steps as a table
Step
Line
What happened
Variables now
1
12
_a is created. Nothing is printed on creation.
alive = a
2
13
_b is created after it.
alive = a, b
3
15
An inner block creates _inner.
alive = a, b, inner
4
16
Still inside the block.
alive = a, b, inner
5
17
The block's closing brace ends _inner's scope, so its drop runs now.
alive = a, b
6
19
drop(c) moves c into a function that ends immediately, so c is freed early.
alive = a, b
7
20
Normal code still runs.
alive = a, b
8
21
End of main: remaining locals are dropped in reverse order of declaration, _b first, then _a.
alive = (none)
rustmain.rs
1234567891011121314151617
structConn{
id:u32,}implDropforConn{fndrop(&mutself){println!("closing conn {}",self.id);}}fnmain(){let pool =vec![Conn{ id:1},Conn{ id:2}];let _ =Conn{ id:9};// bound to nothing: dropped right awaylet _kept =Conn{ id:3};// a real binding: lives to the endprintln!("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
12345678910111213141516171819202122
use std::rc::Rc;
#[derive(Debug)]structConfig{
name:String,}fnmain(){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
12345678
use std::rc::Rc;fnmain(){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
123456789
use std::cell::Cell;use std::rc::Rc;fnmain(){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
12345678910111213141516171819202122232425262728
use std::cell::{Cell,RefCell};structCounter{
hits:Cell<u32>,
log:RefCell<Vec<String>>,}implCounter{fnrecord(&self, msg:&str){// &self, yet it changes stateself.hits.set(self.hits.get()+1);self.log.borrow_mut().push(msg.to_string());}}fnmain(){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: fineprintln!("{} {}", 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.
use std::cell::RefCell;fnmain(){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
12345678
use std::cell::RefCell;fnmain(){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
12345678910111213141516171819202122
use std::cell::RefCell;use std::rc::Rc;
#[derive(Debug)]structAccount{
owner:String,
balance:i64,}fnmain(){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.
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.
Type
Owners
Mutation
Checked
Threads
Box<T>
One
Yes, if you own it mutably
Compile time
Yes, if T is Send
Rc<T>
Many
No
Compile time
No
Weak<T>
None (observer)
No
upgrade() may be None
No
Cell<T>
One
Yes, via get/set (Copy values)
Nothing to check
No
RefCell<T>
One
Yes, via borrow_mut
Run time (panics)
No
Arc<T>
Many
No
Compile time
Yes
Mutex<T> / RwLock<T>
One
Yes, via lock / write
Run 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
123456789101112131415161718
use std::thread;fnmain(){letmut handles =Vec::new();for id in1..=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 orderfor 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
123456789
use std::thread;fnmain(){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
123456789
use std::thread;fnmain(){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 sendertx and a receiverrx. "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
123456789101112131415161718
use std::sync::mpsc;use std::thread;fnmain(){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 goneprintln!("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
123456789101112131415161718192021
use std::sync::mpsc;use std::thread;fnmain(){let(tx, rx)= mpsc::channel();for worker in1..=3{let tx = tx.clone();// one sender per thread
thread::spawn(move||{for n in0..2{
tx.send(format!("w{}-{}", worker, n)).unwrap();}});}drop(tx);// drop the original, or rx waits foreverletmut results:Vec<String>= rx.iter().collect();
results.sort();// arrival order varies; sort before printingprintln!("{} messages", results.len());println!("{:?}", results);}
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
1234567891011121314151617181920212223
use std::sync::{Arc,Mutex};use std::thread;fnmain(){let total =Arc::new(Mutex::new(0));letmut handles =Vec::new();for i in1..=4{let total =Arc::clone(&total);
handles.push(thread::spawn(move||{for _ in0..1000{letmut 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
12345678910111213141516
use std::sync::Mutex;use std::thread;fnmain(){let total =Mutex::new(0);letmut handles =Vec::new();for i in1..=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
1234567891011121314151617
use std::sync::{Arc,Mutex};use std::thread;fnmain(){let total =Arc::new(Mutex::new(0));letmut handles =Vec::new();for i in1..=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
123456789101112131415161718
use std::sync::{Arc,RwLock};use std::thread;fnmain(){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
1234567891011
use std::rc::Rc;use std::thread;fnmain(){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
1234567891011
use std::sync::Arc;use std::thread;fnmain(){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
1234567891011121314151617
use std::thread;fnmain(){let words =vec!["alpha","beta","gamma","delta"];letmut 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 hereprintln!("{:?}", words);// still usable: only borrowedprintln!("{:?}", 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?
Rc is not Send, and RefCell is not Sync, so neither can be shared across threads. A cloned Box gives each thread its own counter. Arc shares ownership and Mutex makes the updates exclusive (an AtomicU64 inside an Arc would also work).
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.
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.