Every way to run things at the same time
Threads, scoped threads, channels, Arc and Mutex, atomics, rayon, async tasks and spawn_blocking. Which one fits the work you have.
First decide what kind of waiting the program does.
- The CPU is the bottleneck (resizing images, parsing a big log)
→ threads. If the same work runs on every item, rayon's
par_iterdoes the splitting for you. If you want it by hand, scoped threads can borrow the data withoutArc. - The program mostly waits on the network (a web server, a crawler with thousands of connections) → async tasks on a runtime such as tokio. Thousands of tasks cost far less than thousands of threads.
- A few long-running jobs that talk to each other → threads with channels between them: each value moves to the thread that uses it.
- Several threads must read and update the same value →
Arc<Mutex<T>>,RwLockwhen reads dominate, or an atomic for a single counter or flag. - Async code has to call something slow and blocking (a
password hash, a synchronous library) →
spawn_blocking, so it doesn't stall every other task.
Share nothing where you can. Each step towards shared, changeable state adds a lock to hold, and a chance of deadlock.
Threads
Section
Threads
spawn, join, and what happens to a panic inside one.
Section
move closures and scoped threads
A spawned thread must own its data; thread::scope lets it borrow instead.
Formal. A type is Send if a value of it can be moved to another
thread, and Sync if a shared reference &T can be used from several
threads at once (equivalently, &T: Send). Both are auto traits: a
struct is Send/Sync exactly when all its fields are. thread::spawn
requires its closure (and the value it returns) to be Send + 'static;
violating that is a compile error (E0277). Rc and RefCell are not
Sync, and Rc is not Send; Arc<T> is Send + Sync when T is
Send + Sync, and the lock types of the next chapter make shared data Sync.
In plain English. The compiler labels every type "safe to hand to another thread" and "safe to share between threads", works the labels out from what the type contains, and refuses to compile a program that would move or share something without the right label.
Example. Spawning a thread with a move closure that captures an
Rc<Vec<u32>> is E0277; with an Arc<Vec<u32>> it compiles.
Why this rule exists. Data races are undefined behavior; checking thread safety in the type system turns them into compile errors instead of rare, unreproducible bugs.
The two marker traits that decide what may cross a thread boundary.
Formal. A data race happens when (1) two or more accesses to the same memory occur at the same time, (2) at least one of them writes, and (3) nothing synchronizes them. Its outcome is undefined. Remove any one ingredient and there is no data race.
In plain English. Trouble needs all three: two things touching the same data at once, one of them changing it, and nothing making them take turns.
Example. The borrowing rules remove ingredient (2) whenever there is more than one reference (many readers, no writer) and ingredient (1) whenever there is a writer (only one reference exists).
Why this rule exists. Data races produce bugs that appear only sometimes and are very hard to reproduce. Knowing the three ingredients explains exactly what Rust's rules are preventing.
Two accesses, at least one a write, with no synchronization. Safe Rust rules out the combination.
Pass messages
Section
Channels
mpsc between threads. The sender's value moves to the receiver.
Section
Async channels
The same idea for tasks, with bounded capacity for back-pressure.
Share state
Section
Arc and Mutex
Shared ownership plus a lock around the data.
Section
Atomics
A counter or flag with no lock, and what memory ordering means.
Section
Deadlocks and starvation
The bugs Rust doesn't prevent, and the lock-ordering rule that does.
Section
Lazy and global state
OnceLock and LazyLock for a value set up once and shared everywhere.
Split the data
Section
Parallel data processing
A slice split across scoped threads, then the same job with rayon.
Example
lines.par_iter().filter(|l| l.contains("ERROR")).count() counts matching lines on every core with one change from iter.Async tasks
Section
Tasks
tokio::spawn, and why a task must be 'static and Send.
Section
Concurrency versus parallelism
Taking turns while you wait, versus working on two cores at once.
Section
Joining and selecting
join! for all of them, select! for the first, JoinSet for a changing number.
Section
Blocking and locks in async
spawn_blocking for slow synchronous work, and which Mutex to use inside a task.
Choosing
Section
Choosing a strategy
Four ways to share work between threads: share nothing, send messages, lock, or use atomics.
When plain threads are the simpler and faster answer.