Concurrency
A program so far has done one thing at a time. A thread lets part of it run at the same time as the rest: on a machine with several cores, threads really do run in parallel, so work split across four threads can finish close to four times faster. The cost is that two threads can touch the same data at the same moment, and in most languages that leads to data races, bugs that only show up now and then and are very hard to reproduce.
Rust's ownership and borrowing rules extend to threads. Code that could race
on data does not compile. The Rust community calls this fearless
concurrency. This lesson covers the tools in the standard library: spawning
threads, passing messages between them with channels, and sharing data with
Arc and Mutex.
Spawning a thread
std::thread::spawn takes a closure and runs it on a new thread. The thread
that runs main carries on with its own work in the meantime:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for step in 1..=3 {
println!("spawned thread: step {step}");
thread::sleep(Duration::from_millis(1));
}
});
for step in 1..=3 {
println!("main thread: step {step}");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}One run printed:
main thread: step 1
spawned thread: step 1
main thread: step 2
spawned thread: step 2
main thread: step 3
spawned thread: step 3Run it a few times and the order changes. The operating system decides when each thread runs, and your program can't rely on any particular interleaving.
spawn returns a JoinHandle. Calling join() on it blocks until that
thread has finished. Without the join, main could return first, and when
main returns the whole process exits, taking any unfinished threads with it.
Getting a value back
The closure's return value is not lost: join() hands it back. A thread that
returns a usize gives you a JoinHandle<usize>:
use std::thread;
fn main() {
let handle = thread::spawn(|| (1..=1_000_000_u64).filter(|n| n % 7 == 0).count());
println!("counting in the background...");
let count = handle.join().unwrap();
println!("{count} multiples of 7");
}counting in the background...
142857 multiples of 7join() returns a Result. It is Err when the thread panicked, so a panic
in one thread does not bring down the thread that joins it:
use std::thread;
fn main() {
let handle = thread::spawn(|| {
let readings: Vec<u32> = Vec::new();
readings[0]
});
match handle.join() {
Ok(first) => println!("first reading: {first}"),
Err(_) => println!("the worker panicked"),
}
println!("main keeps going");
}the worker panicked
main keeps goingThe panic message (index out of bounds) still goes to stderr. Most code
calls .unwrap() on join(), which passes a worker's panic on to the thread
that joined it.
move closures
A spawned thread can run for longer than the function that created it, so its
closure is not allowed to borrow that function's local variables. This closure
only reads words, so it tries to borrow it:
use std::thread;
fn main() {
let words = vec!["ferris", "crab", "rustacean"];
let handle = thread::spawn(|| {
let longest = words.iter().max_by_key(|w| w.len()).unwrap();
println!("longest word: {longest}");
});
handle.join().unwrap();
}The compiler doesn't look at the join() below; as far as the type of spawn
is concerned, the thread might still be running after main has dropped
words. The fix is the one it suggests: a move closure takes ownership of
what it captures, so the data goes to the thread and lives as long as it does.
use std::thread;
fn main() {
let words = vec!["ferris", "crab", "rustacean"];
let handle = thread::spawn(move || {
let longest = words.iter().max_by_key(|w| w.len()).unwrap();
println!("longest word: {longest}");
});
handle.join().unwrap();
}longest word: rustaceanAfter the move, words belongs to the thread, and main can't use it any
more. If both need it, clone it before spawning, or share it with Arc
(below).
This rule is in the signature of spawn:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,'static means the closure holds no borrowed data that could go away, and
Send means it is safe to hand to another thread (more on Send at the end).
The same bounds apply to the value it returns.
Several threads
To split work, spawn a thread per piece, keep the handles in a Vec, and join
them all. Here four threads each count the primes in a quarter of a range:
use std::thread;
fn is_prime(n: u64) -> bool {
n >= 2 && (2..).take_while(|d| d * d <= n).all(|d| n % d != 0)
}
fn main() {
let mut handles = Vec::new();
for chunk in 0..4 {
let start = chunk * 25_000;
let end = start + 25_000;
handles.push(thread::spawn(move || {
(start..end).filter(|&n| is_prime(n)).count()
}));
}
let mut total = 0;
for (chunk, handle) in handles.into_iter().enumerate() {
let count = handle.join().unwrap();
println!("chunk {chunk}: {count} primes");
total += count;
}
println!("total: {total} primes below 100000");
}chunk 0: 2762 primes
chunk 1: 2371 primes
chunk 2: 2260 primes
chunk 3: 2199 primes
total: 9592 primes below 100000The threads finish in any order, but the results come out in order, because
the handles are joined in the order they were spawned. move copies start
and end into each closure, so every thread gets its own.
Threads are not free: each has its own stack and costs some time to start. A
thread per core is a good rule of thumb, and
thread::available_parallelism() tells you how many that is. Spawning one per
tiny piece of work is usually slower than a plain loop.
Scoped threads
Sometimes the threads only need to borrow data for a while, and moving or
cloning it is a waste. thread::scope spawns threads that are guaranteed to
finish before scope returns, so they can borrow local variables:
use std::thread;
fn main() {
let temperatures = vec![18.5, 21.0, 19.5, 23.0, 22.5, 17.0];
let (morning, evening) = temperatures.split_at(3);
let (low, high) = thread::scope(|s| {
let low = s.spawn(|| morning.iter().copied().fold(f64::INFINITY, f64::min));
let high = s.spawn(|| evening.iter().copied().fold(f64::NEG_INFINITY, f64::max));
(low.join().unwrap(), high.join().unwrap())
});
println!("morning low: {low}, evening high: {high}");
println!("still usable: {} readings", temperatures.len());
}morning low: 18.5, evening high: 23
still usable: 6 readingsNo move, no clone: both threads borrow slices of temperatures, and main
still owns the vector afterwards. Any scoped thread you don't join is joined
automatically at the end of the scope. Reach for thread::scope when the work
fits inside one function call, and thread::spawn when a thread has to
outlive it.
Message passing with channels
One way for threads to cooperate is to not share data at all, and send it
instead. A channel has two ends: a sender (tx) and a receiver (rx).
Values sent into one end come out of the other in the same order.
std::sync::mpsc stands for multiple producer, single consumer: there can
be many senders but only one receiver.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for file in ["intro.md", "setup.md", "faq.md"] {
tx.send(format!("uploaded {file}")).unwrap();
}
});
for message in rx {
println!("{message}");
}
println!("all uploads done");
}uploaded intro.md
uploaded setup.md
uploaded faq.md
all uploads doneA few things are going on:
mpsc::channel()returns the pair(Sender<T>, Receiver<T>).Tis inferred from what you send, hereString.sendmoves the value into the channel. Ownership goes with it, so the sending thread can't use the value afterwards, and the two threads never hold it at the same time.for message in rxwaits for each message in turn. The loop ends when every sender has been dropped, which happens here when the thread's closure finishes.
The receiver has more than the loop: rx.recv() waits for one value and
returns Err once all senders are gone, rx.try_recv() returns straight away
whether or not something has arrived, and rx.recv_timeout(duration) waits at
most that long. On the other side, send returns Err if the receiver has
been dropped, since nobody would ever read the value.
Several producers
To have several threads send to the same receiver, clone the sender, one clone per thread:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let stations = [
("north", vec![2, 0, 5]),
("south", vec![1, 1, 1]),
("east", vec![7, 3, 0]),
];
for (station, samples) in stations {
let tx = tx.clone();
thread::spawn(move || {
let total: u32 = samples.iter().sum();
tx.send((station, total)).unwrap();
});
}
drop(tx);
let mut totals: Vec<(&str, u32)> = rx.iter().collect();
totals.sort();
for (station, mm) in totals {
println!("{station}: {mm} mm of rain");
}
}east: 10 mm of rain
north: 7 mm of rain
south: 3 mm of rainEach thread gets its own tx (the let tx = tx.clone(); shadows the
original inside the loop body). The messages arrive in whatever order the
threads finish, so the program sorts them before printing.
Note the drop(tx). The receiver stops only when every sender is gone,
and main still holds the original one. Leave that line out and
rx.iter() waits forever for a message that will never come: the program
compiles, prints nothing and hangs.
mpsc::channel is unbounded: senders never wait. mpsc::sync_channel(n)
makes a channel that holds at most n values, and send blocks when it is
full, which keeps a fast producer from running far ahead of a slow consumer.
Shared state with Mutex
Channels move data between threads. Sometimes several threads really do need
to read and change the same value. A Mutex<T> (mutual exclusion) guards a
value so that only one thread at a time can reach it:
use std::sync::Mutex;
fn main() {
let shelf = Mutex::new(vec!["apples", "pears"]);
{
let mut items = shelf.lock().unwrap();
items.push("plums");
}
println!("{:?}", shelf.lock().unwrap());
}["apples", "pears", "plums"]lock() waits until no other thread holds the lock, then returns a
MutexGuard. The guard derefs to the value inside, so you use it like a
&mut Vec. When the guard is dropped, here at the end of the inner block, the
lock is released. There is no unlock to forget.
lock() returns a Result because a mutex can be poisoned: if a thread
panics while holding the lock, the value might be half-updated, and later
lock() calls return Err. Calling unwrap() on it is the usual choice,
since it turns that into a panic in the next thread too.
Note that shelf is not mut. Like RefCell, a Mutex gives you mutable
access through a shared reference, and checks at runtime that access is
exclusive. The difference is that a RefCell panics when the rule is broken,
and a Mutex makes the second thread wait.
Hold the lock for the whole of an operation that must not be interrupted. Here, checking the balance and taking money out happen under one lock, so two threads can't both see enough money and both withdraw it:
use std::sync::Mutex;
use std::thread;
fn withdraw(balance: &Mutex<u32>, amount: u32) -> bool {
let mut balance = balance.lock().unwrap();
if *balance >= amount {
*balance -= amount;
true
} else {
false
}
}
fn main() {
let balance = Mutex::new(100);
let succeeded = thread::scope(|s| {
let handles: Vec<_> = (0..5).map(|_| s.spawn(|| withdraw(&balance, 30))).collect();
let results = handles.into_iter().map(|h| h.join().unwrap());
results.filter(|&ok| ok).count()
});
println!("{succeeded} withdrawals succeeded");
println!("left: {}", balance.lock().unwrap());
}3 withdrawals succeeded
left: 10Five threads try to take 30 from 100. Whichever order they run in, exactly
three succeed. Scoped threads could borrow balance directly, because they
all finish inside thread::scope.
Arc: sharing ownership across threads
Threads from thread::spawn can't borrow, so each one needs to own the
mutex. The smart pointers lesson solved "several owners" with Rc, but Rc
updates its count with plain arithmetic, which two threads could do at the
same moment and get wrong. Arc<T> (atomically reference counted) is the
same idea with a thread-safe count. Arc::clone gives each thread its own
pointer to one shared value:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let done = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for job in ["resize", "compress", "upload"] {
let done = Arc::clone(&done);
handles.push(thread::spawn(move || {
// the job's real work would happen here
done.lock().unwrap().push(job);
}));
}
for handle in handles {
handle.join().unwrap();
}
let mut finished = done.lock().unwrap();
finished.sort();
println!("finished: {finished:?}");
println!("owners left: {}", Arc::strong_count(&done));
}finished: ["compress", "resize", "upload"]
owners left: 1The pattern is always the same: clone the Arc before the move closure,
so the closure takes the clone and the original stays in main. When a
thread finishes, its clone is dropped, which is why only one owner is left at
the end.
Arc on its own gives shared, read-only access, like Rc. For data that
threads only read, such as settings or a lookup table, Arc<T> is all you
need. Add the Mutex when they also write.
Other tools in std::sync for the same job:
| Type | Use it for |
|---|---|
Arc<T> | several threads reading the same data |
Arc<Mutex<T>> | several threads reading and writing |
Arc<RwLock<T>> | many readers at once, or one writer (read() and write()) |
AtomicUsize, AtomicBool | a single number or flag, updated without a lock |
mpsc::channel | handing values from producers to one consumer, no sharing |
A thread that holds one lock and waits for another, while a second thread does the opposite, waits forever: a deadlock. The compiler does not catch those. Keep guards short-lived, and when a thread needs two locks, take them in the same order everywhere.
Send and Sync
How does the compiler know Arc is safe to send to a thread and Rc is not?
Through two marker traits, which have no methods:
Send: a value of this type can be moved to another thread.Sync: a value of this type can be shared between threads, that is,&TisSend.
Both are implemented automatically: a struct is Send if all its fields are.
Almost every type is both. The exceptions are the ones that would be unsound
across threads:
| Type | Send | Sync | Why |
|---|---|---|---|
Rc<T> | no | no | its count is not updated atomically |
RefCell<T> | yes | no | its borrow flag is not updated atomically |
Arc<T> | yes | yes | when T is Send + Sync |
Mutex<T> | yes | yes | when T is Send |
thread::spawn requires Send for the closure and its return value, as its
signature above shows. So a generic function that spawns threads with a T
has to promise the same, with a bound like T: Send + 'static. You will
rarely implement Send or Sync yourself (doing so is unsafe); mostly you
meet them in error messages, when a type is not allowed to cross a thread.
Common mistakes
Moving the sender into the first thread
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
for worker in 1..=3 {
thread::spawn(move || {
tx.send(worker * 10).unwrap();
});
}
let total: u32 = rx.iter().sum();
println!("{total}");
}The first iteration's move closure takes tx, and the second has nothing
left to take. Give each thread its own sender with let tx = tx.clone(); at
the top of the loop body, and drop(tx) after the loop so that rx.iter()
ends.
Sending an Rc to a thread
use std::rc::Rc;
use std::thread;
fn main() {
let settings = Rc::new(String::from("dark mode"));
let for_thread = Rc::clone(&settings);
let handle = thread::spawn(move || {
println!("theme: {for_thread}");
});
handle.join().unwrap();
}Rc is not Send, and spawn requires it. Swap Rc for Arc
(use std::sync::Arc;); the rest of the code stays the same.
Using the value without locking
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let log = Arc::new(Mutex::new(Vec::new()));
let writer = Arc::clone(&log);
thread::spawn(move || {
writer.push("started");
})
.join()
.unwrap();
println!("{:?}", log.lock().unwrap());
}An Arc derefs to what it holds, which here is the Mutex, not the Vec.
The only way to the Vec is through a lock:
writer.lock().unwrap().push("started"). That is the point of a mutex: you
can't forget to lock it.
A guard that is not mut
use std::sync::Mutex;
fn main() {
let queue = Mutex::new(vec![3, 1, 2]);
let guard = queue.lock().unwrap();
guard.sort();
println!("{guard:?}");
}The mutex itself doesn't need mut, but changing the value goes through the
guard's DerefMut, and that needs a mutable binding: let mut guard.
Summary
thread::spawn(closure)runs the closure on a new thread and returns aJoinHandle.join()waits for the thread and returns its value, orErrif it panicked. Whenmainreturns, unfinished threads are stopped.- Threads run in an order you don't control. Don't rely on it; join the handles or sort the results.
- A spawned closure must be
'static, so usemoveto give it ownership of what it uses.thread::scopelets threads borrow local data instead. - Channels (
mpsc::channel) send values from any number of senders to one receiver. Clone the sender per thread, and drop every sender so the receiver's loop ends. Mutex<T>gives one thread at a time access to a value; the lock is released when the guard is dropped.Arc<T>shares ownership across threads, andArc<Mutex<T>>is shared, mutable data.SendandSyncmark the types that may cross threads. The compiler checks them, which is whyRcandRefCellare rejected whereArcandMutexare accepted.
Practise what you read
Challenges that use the ideas from this lesson.