Part III — Advanced: concurrency, async, unsafe
The ownership rules from Part I matter most in this part. The same rule,
shared XOR mutable, that prevents iterator invalidation also makes data
races a compile error. This Part also covers async Rust, which is how Rust
handles tens of thousands of connections, and unsafe, which is how Rust
talks to C and the kernel.
28. Threads, and why data races do not compile
28.1 Spawning and joining
use std::thread;
use std::time::Duration;
fn main() {
let handles: Vec<thread::JoinHandle<u64>> = (0..4)
.map(|id| {
thread::spawn(move || { // `move`: the thread owns its copy of `id`
thread::sleep(Duration::from_millis(10 * id));
(1..=1_000_000u64).filter(|n| n % (id + 2) == 0).count() as u64
})
})
.collect();
// join() returns Result: Err if the thread panicked.
let results: Vec<u64> = handles.into_iter().map(|h| h.join().expect("worker panicked")).collect();
println!("{results:?}");
println!("cores available: {:?}", thread::available_parallelism()); // respects cgroup CPU limits on Linux
}
std::thread creates one OS thread per spawn (1:1), the same as
pthread_create in the C guide §20.
Each thread gets about 2 MiB of stack by default (Rust's default, configurable
with thread::Builder::stack_size). Go's goroutines are M:N and start at a few KiB
(Go §15).
Rust's version of cheap tasks is async (Ch 30).
Across the series: what a thread is to the kernel, and what a context switch costs, is in OS Ch 7 and OS Ch 9.
28.2 The data race that will not compile
// DOES NOT COMPILE
use std::thread;
fn main() {
let mut counter = 0;
let mut handles = vec![];
for _ in 0..4 {
handles.push(thread::spawn(|| {
for _ in 0..1000 { counter += 1; } // four threads writing one variable
}));
}
for h in handles { h.join().unwrap(); }
println!("{counter}");
}
error[E0499]: cannot borrow `counter` as mutable more than once at a time
error[E0373]: closure may outlive the current function, but it borrows `counter`
The C version of this program compiles and prints a different wrong number
on each run (OS Ch 17
shows the same race with shell processes). The Go version compiles, and
go run -race catches it if the race happens during that run. Rust
rejects it at compile time, because four &mut counter borrows would exist
at the same time.
28.3 Send and Sync
Two marker traits, implemented automatically by the compiler, encode thread safety in the type system:
T: Send: aTcan be moved to another thread. Almost everything isSend.Rc<T>is not: its reference count is non-atomic, so two threads cloning it would race on the counter.T: Sync: a&Tcan be shared between threads, that is,&TisSend.RefCell<T>andCell<T>are notSync, because their runtime borrow tracking is not atomic.Mutex<T>isSync.
thread::spawn requires F: Send + 'static. That one bound is what makes
the next program fail:
// DOES NOT COMPILE
use std::rc::Rc;
fn main() {
let shared = Rc::new(vec![1, 2, 3]);
let s2 = Rc::clone(&shared);
std::thread::spawn(move || println!("{s2:?}")).join().unwrap();
}
error[E0277]: `Rc<Vec<i32>>` cannot be sent between threads safely
= help: within `{closure}`, the trait `Send` is not implemented for `Rc<Vec<i32>>`
Replace Rc with Arc, which uses atomic counters, and it compiles. The
compiler does not know anything special about threads. Thread safety
follows from two traits and a bound on spawn.
28.4 Scoped threads: borrowing across threads
fn main() {
let data: Vec<u64> = (1..=10_000).collect();
let mut partial = [0u64; 4];
// Threads spawned in a scope are guaranteed to be joined before the scope ends,
// so they may BORROW local data. No Arc and no 'static needed.
std::thread::scope(|s| {
for (chunk, slot) in data.chunks(data.len() / 4).zip(partial.iter_mut()) {
s.spawn(move || *slot = chunk.iter().sum());
}
});
println!("{partial:?} total={}", partial.iter().sum::<u64>()); // total=50005000
}
chunks gives each thread a disjoint &[u64], and iter_mut gives each
thread its own &mut u64. Shared XOR mutable holds, so no locks are needed.
For data-parallel loops, the rayon crate turns .iter() into
.par_iter() and handles the work-splitting for you.