</>
Skip to content
Rust lessons (40/43)

Rust — Threads

Real parallelism, safely

thread::spawn starts an OS thread — code genuinely runs simultaneously on other cores:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("worker: {}", i);
        }
    });

    println!("main still running");

    handle.join().unwrap();     // wait for the worker to finish
}

Without join(), main may exit first and kill the worker mid-flight. join blocks until the thread completes.

Moving data into threads

Closures capture environment; threads outlive ambiguity, so Rust demands move:

let numbers = vec![1, 2, 3];

let handle = thread::spawn(move || {
    println!("got {:?}", numbers);   // ownership transferred INTO the thread
});

// println!("{:?}", numbers);       ❌ moved!
handle.join().unwrap();

move forces ownership transfer — the borrow checker guarantees the thread never references freed stack data. This single keyword eliminates a whole C++ footgun class.

Returning values from threads

JoinHandle returns the closure's result:

let result = thread::spawn(move || {
    (1..=100).sum::<i64>()
}).join().unwrap();

println!("sum: {}", result);   // 5050

.join() returns Result<T> (thread may panic) — unwrap() or propagate.

Sharing MUTABLE data — Arc<Mutex<T>>

Two threads can't both own one value. The standard pattern wraps it:

use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);   // cheap ref-counted clone
    handles.push(thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    }));                                   // MutexGuard unlocks on drop
}

for h in handles { h.join().unwrap(); }

println!("{}", *counter.lock().unwrap());   // 10 — always exactly 10
PieceJob
ArcAtomic Reference Counting — share across threads
Mutexexclusive access — only one thread mutates at a time
.lock().unwrap()acquire; blocks until free
guard dropautomatic unlock at end of scope

Rust makes you prove safety through types instead of hoping discipline holds — data races are compile errors here.

Channels — passing messages instead

Often better than shared state: threads communicate by sending values:

use std::sync::mpsc;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    tx.send(42).unwrap();
});

println!("{}", rx.recv().unwrap());   // 42

Full treatment in the channels lesson.

Threads vs async (preview)

OS threadsasync/await
Cost~MB stack eachtiny task structs
Best forCPU-bound workthousands of I/O waits
Runtimebuilt into stdneeds tokio etc.

Mini Practice

  1. Spawn 4 threads each printing their id 3 times; join all.
  2. Move a vector in; sum inside; return via join.
  3. Race condition demo without Mutex (compile error proves it) — then fix with Arc<Mutex>.
  4. Channel: producer sends 1..=5, main receives all.

Next: channels →

Related Topics

Frequently Asked Questions about Threads

What is Threads in Rust?

Threads is a fundamental concept in Rust. This lesson explains it step by step with clear examples, making it easy for beginners to understand.

How do I learn Threads?

Start by reading the explanation above, then try the code examples. Practice by modifying the examples and experimenting with different values. Hands-on practice is the best way to learn Threads.

Why is Threads important in Rust?

Threads is essential for Rust development. Understanding this concept will help you write better code and solve real-world problems more effectively.