Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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_iter does the splitting for you. If you want it by hand, scoped threads can borrow the data without Arc.
  • 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>>, RwLock when 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.

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.

The bugs Rust doesn't prevent, and the lock-ordering rule that does.

OnceLock and LazyLock for a value set up once and shared everywhere.

Split the data

A slice split across scoped threads, then the same job with rayon.

Doing the same work on many pieces of data at once, on several CPU cores.

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.

Taking turns while you wait, versus working on two cores at once.

join! for all of them, select! for the first, JoinSet for a changing number.

spawn_blocking for slow synchronous work, and which Mutex to use inside a task.

Choosing

Four ways to share work between threads: share nothing, send messages, lock, or use atomics.

When plain threads are the simpler and faster answer.