Skip to content

Concurrency, Testing & Cargo

Fearless Concurrency — What This Module Covers

Section titled “Fearless Concurrency — What This Module Covers”

One of Rust’s boldest claims is fearless concurrency: writing multi-threaded programs without the data races that plague C, C++, or even Java. The compiler enforces the same ownership rules at thread boundaries, turning an entire class of bugs into compile-time errors rather than hard-to-reproduce runtime failures.

This module explores four pillars of production Rust:

LessonTopicRunnable
Threadsstd::thread, Arc<Mutex<T>>, joining handlesYes — browser Playground
Channelsstd::sync::mpsc, send/receive, ownership transferYes — browser Playground
Async Introasync/.await, futures, tokio runtimeCargo project only
Cargo & Testscargo new, #[test], cargo testCargo project only

Rust standard library gives you OS threads via std::thread. Each thread is a real OS-level thread with its own stack. Shared state between threads is protected by Arc<Mutex<T>> — the compiler will not let you pass a non-Send type across a thread boundary.

For I/O-bound workloads — network servers, file operations, many concurrent lightweight tasks — the async/.await model is more efficient. It uses a single-threaded or multi-threaded event loop (usually tokio) to schedule thousands of tasks without the overhead of an OS thread per task.

In most languages, data races are a runtime problem. You write the code, run it, and occasionally it crashes or returns wrong results under load. In Rust:

  • A type is Send if it is safe to move to another thread.
  • A type is Sync if it is safe to share a reference across threads.
  • The compiler rejects code that violates these contracts — no sanitizers, no runtime checks needed.
flowchart TD
  A["What kind of work am I scheduling?"] --> B{"CPU-bound, or I/O-bound?"}
  B -->|CPU-bound| C["std::thread — real OS thread, ~8 MB stack"]
  B -->|I/O-bound, many at once| D["async/.await — lightweight task, needs an executor"]
  C --> E["Share state with Arc + Mutex/RwLock"]
  D --> F["Share state the same way, but never hold a sync lock across .await"]
Choosing between OS threads and async tasks

Here is the smallest meaningful concurrent Rust program — spawn a thread, wait for it, continue in main.

use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Hello from a spawned thread!");
});
handle.join().unwrap();
println!("Main thread done.");
}

What the Compiler Says: Using a Value After It Was Moved Into a Thread

Section titled “What the Compiler Says: Using a Value After It Was Moved Into a Thread”

thread::spawn requires its closure to own everything it captures (via move), because the spawned thread might still be running after the function that created it returns. That ownership transfer is real — the value is genuinely gone from the caller, not just borrowed:

// @expect-error E0382
use std::thread;
fn main() {
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("sum from thread: {}", data.iter().sum::<i32>());
});
handle.join().unwrap();
println!("data length: {}", data.len()); // ERROR: data was moved into the thread above
}
error[E0382]: borrow of moved value: `data`
--> src/main.rs:9:33
|
4 | let data = vec![1, 2, 3];
| ---- move occurs because `data` has type `Vec<i32>`, which does not implement the `Copy` trait
5 | let handle = thread::spawn(move || {
| ------- value moved into closure here
6 | println!("sum from thread: {}", data.iter().sum::<i32>());
| ---- variable moved due to use in closure
...
9 | println!("data length: {}", data.len());
| ^^^^ value borrowed here after move
|
help: consider cloning the value before moving it into the closure
|
5 ~ let value = data.clone();
6 ~ let handle = thread::spawn(move || {
7 ~ println!("sum from thread: {}", value.iter().sum::<i32>());
|

The compiler’s own suggestion is .clone() — and that is a legitimate fix here, since main genuinely needs data in two places. But treat that suggestion as a last resort, not a reflex: the better fix is usually to have the thread compute and return the value main needs (via JoinHandle::join()) instead of a second copy existing on both sides.

Every value in Rust has exactly one owner at a time, and thread::spawn’s closure becomes a new owner the moment you write move. Nothing “magic” happens at the thread boundary — it is the same move-semantics rule that governs passing a String to a function, just enforced against the extra constraint that the closure (and everything it captures) must be 'static and Send, because the compiler cannot know when the spawned thread will finish relative to the rest of the program.

OS threads give you true parallelism and a simple mental model (each thread just runs), at the cost of a real OS stack (~8 MB) and a hard ceiling around hundreds to low thousands of threads. async/.await tasks are ~cheap — you can have thousands to millions of them — but every one of them needs an executor like tokio to actually run, and CPU-bound work on that executor blocks every other task sharing its thread.

  • “Fearless concurrency means Rust prevents all concurrency bugs.” It prevents data races (two threads touching the same memory unsynchronized, at least one write) at compile time. It does not prevent deadlocks, logic races, or lock poisoning after a panic — those are still your responsibility, covered later in this module.
  • “Send and Sync are things you implement yourself for most types.” Almost every type gets both automatically (they are auto traits) as long as every field is Send/Sync. You only think about them when the compiler tells you a bound is not satisfied, usually because of Rc<T>, RefCell<T>, or a raw pointer somewhere inside.
  • “async is always faster than threads.” For CPU-bound work, async adds overhead (polling, state-machine allocation) with no benefit — threads or rayon-style parallelism are the right tool there.

AI-generated concurrent Rust reaches for thread::spawn and pushes the returned JoinHandle into a Vec, but sometimes drops the handle immediately (or never collects the Vec at all) because the surrounding code “looked done” without it.

🐛 Spot the bug

use std::thread;
fn process_all(items: Vec<i32>) {
for item in items {
thread::spawn(move || {
println!("processed: {}", item * 2);
});
}
println!("all items processed");
}
Show the answer

thread::spawn returns a JoinHandle, and this code never stores it — the handle is dropped immediately, which detaches the thread but does not wait for it. process_all can print "all items processed" and return before any spawned thread has run at all; if main exits right after, the process can terminate mid-work with some (or all) of the println!("processed: ...") lines never printed. The fix: collect every JoinHandle into a Vec and call .join().unwrap() on each one before returning.

What does it mean for a type to be Send in Rust?
Which standard library type is used to share mutable state safely between threads?
When should you prefer async/await over OS threads?
What happens when this compiles and runs?
use std::thread;
fn main() {
    let data = vec![1, 2, 3];
    let h = thread::spawn(move || {
        println!("{}", data.len());
    });
    h.join().unwrap();
    println!("{}", data.len());
}
In the SpotTheBug example, why can "all items processed" print before any spawned thread finishes?