- Store a value on the heap with
Boxand build a recursive type - Create several owners with
Rcand interior mutability withRefCell - Start threads with
moveclosures and collect results withjoin - Share data between threads with channels and
Arc<Mutex<T>>
Until now, every value had a single owner. But sometimes you need something else: a recursive structure whose size is unknown in advance, the same song in several playlists, changing shared data, or splitting work across several processor cores. For this, Rust has smart pointers and concurrency tools — and they obey the ownership rules too.
`Box<T>`: a value on the heap
Box::new(x) puts a value on the heap, and only a fixed-size pointer stays on the stack. When the box's owner goes out of scope, the value on the heap is dropped automatically too. Box is needed in three cases: recursive types whose size is unknown at compile time, trait objects such as Box<dyn Trait>, and large data that is expensive to move around on the stack.
enum List {
Node(i32, List),
Empty,
}enum List {
Node(i32, Box<List>),
Empty,
}List contains a List inside itself, so the compiler says: recursive type List has infinite size. A Box is a fixed-size pointer and breaks the cycle.enum List {
Node(i32, Box<List>),
Empty,
}
use List::{Empty, Node};
fn sum(list: &List) -> i32 {
match list {
Node(value, next) => value + sum(next),
Empty => 0,
}
}
fn main() {
let boxed = Box::new(5);
println!("boxed + 1 = {}", *boxed + 1);
let list = Node(1, Box::new(Node(2, Box::new(Node(3, Box::new(Empty))))));
println!("sum = {}", sum(&list));
}boxed + 1 = 6 sum = 6
`Rc<T>`: several owners
Rc (reference counting) lets one value have several owners. Rc::clone(&x) does not copy the data; it only increases a counter by one, which is very cheap. Each time an owner is dropped the counter goes down, and when it reaches zero the value is dropped. Rc only allows reading the value and works only within a single thread.
use std::rc::Rc;
fn main() {
let song = Rc::new(String::from("lesson.mp3"));
println!("owners: {}", Rc::strong_count(&song));
let playlist_a = Rc::clone(&song);
{
let playlist_b = Rc::clone(&song);
println!("owners: {}", Rc::strong_count(&song));
println!("b plays {playlist_b}");
}
println!("owners: {}", Rc::strong_count(&song));
println!("a plays {playlist_a}");
}owners: 1 owners: 3 b plays lesson.mp3 owners: 2 a plays lesson.mp3
playlist_b was dropped when the inner block ended, and the counter went from 3 to 2. The text itself is stored in memory only once.`RefCell<T>`: interior mutability
RefCell checks the borrowing rules while the program runs instead of at compile time. borrow() lends the value for reading, and borrow_mut() for changing. This lets you change a value behind an immutable reference. The combination Rc<RefCell<T>> is the typical solution: several owners can both read and change shared data.
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let scores = Rc::new(RefCell::new(vec![80, 95]));
let teacher = Rc::clone(&scores);
teacher.borrow_mut().push(70);
scores.borrow_mut().push(100);
println!("{:?}", scores.borrow());
let total: i32 = teacher.borrow().iter().sum();
println!("total = {total}");
}[80, 95, 70, 100] total = 345
| Type | Owners | Mutation | Shared across threads |
|---|---|---|---|
Box<T> | one | if the owner is mut | yes |
Rc<T> | several | no | no |
RefCell<T> | one | yes, checked at run time | no — it can be moved to another thread but not shared (it is not Sync) |
Arc<T> | several | no | yes |
Arc<Mutex<T>> | several | yes, through a lock | yes |
Threads
thread::spawn runs a closure in a new thread and returns a JoinHandle. join() waits for the thread to finish and gives back its result. The move in front of the closure hands ownership of the variables it uses to the thread. Note: lines that threads print themselves may appear in a different order on every run. That is why, below, the threads return results and main prints them in a fixed order.
use std::thread;
fn main() {
let mut handles = Vec::new();
for id in 1..=3_u64 {
let handle = thread::spawn(move || {
let sum: u64 = (1..=id * 1000).sum();
(id, sum)
});
handles.push(handle);
}
for handle in handles {
let (id, sum) = handle.join().unwrap();
println!("thread {id}: sum = {sum}");
}
}thread 1: sum = 500500 thread 2: sum = 2001000 thread 3: sum = 4501500
use std::thread;
fn main() {
let names = vec!["Aysel", "Murad"];
let handle = thread::spawn(|| {
println!("{:?}", names);
});
handle.join().unwrap();
}use std::thread;
fn main() {
let names = vec!["Aysel", "Murad"];
let handle = thread::spawn(move || {
println!("{:?}", names);
});
handle.join().unwrap();
}closure may outlive the current function, but it borrows names. The thread could live longer than main, so a reference is not enough. The program on the right prints ["Aysel", "Murad"].Channels and `Arc<Mutex<T>>`
There are two ways to share data between threads. The first is a channel: mpsc::channel() creates a transmitter (tx) and a receiver (rx); one thread sends with send and another receives. You can loop over the receiver with for — the loop ends when all transmitters have been dropped. Messages from a single sender arrive in the order they were sent.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let worker = thread::spawn(move || {
for step in ["download", "unpack", "install"] {
tx.send(step).unwrap();
}
});
for message in rx {
println!("got: {message}");
}
worker.join().unwrap();
println!("done");
}got: download got: unpack got: install done
The second way is shared state. Arc is the thread-safe (atomic) version of Rc, and Mutex is a lock: the thread that calls lock() gets sole access to the data, and the lock is released automatically when the returned guard goes out of scope. Together, Arc<Mutex<T>> lets several threads change the same value, one at a time.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut n = counter.lock().unwrap();
*n += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}Result: 10
Arc — written with shadowing: let counter = Arc::clone(&counter);.Key points
Box<T>keeps a value on the heap; it is needed for recursive types anddyn Trait.Rc<T>gives several owners within one thread;Rc::cloneonly increases a counter.RefCell<T>checks the borrowing rules at run time; breaking them causes a panic.thread::spawn(move || ...)starts a thread andjoin()waits for its result; print in a fixed order frommain.- Channels pass messages, while
Arc<Mutex<T>>gives shared mutable state between threads.
Check yourself
10 questions. Every correct answer earns XP.
enum List { Node(i32, List), Empty } not compile?