Skip to content
Threads and Send/Sync
step 1/6

Reading — step 1 of 6

Learn

~2 min readModules and Threads

Rust's concurrency story is built on the borrow checker — most data races become compile errors. The std::thread module provides the basics; Send and Sync traits gate which types can cross thread boundaries.

Spawning threads

use std::thread;

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

for i in 1..=5 {
    println!("main: {}", i);
}

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

thread::spawn returns a JoinHandle. Call .join() to wait for it. The closure runs concurrently with the main thread.

Moving data into threads

By default, threads can't borrow from the spawning scope:

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

// thread::spawn(|| { println!("{:?}", data); });   // ✗ might outlive data

thread::spawn(move || {
    println!("{:?}", data);   // ✓ moved into thread
}).join().unwrap();

move forces the closure to take ownership. Required for cross-thread closures since the spawner can't track lifetimes.

Send and Sync — automatic traits

  • Send — safe to TRANSFER ownership across threads. Most types are Send (anything that doesn't have OS-thread-specific state).
  • Sync — safe to SHARE references across threads (i.e., &T is Send).

Both are auto-derived. The compiler checks them silently.

use std::rc::Rc;
use std::sync::Arc;

let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); });   // ✗ Rc isn't Send

let arc = Arc::new(42);
thread::spawn(move || { println!("{}", arc); });    // ✓ Arc is Send

The compiler refuses to compile code that violates Send/Sync — eliminates whole classes of data races.

Shared state via Mutex

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

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

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        let mut c = counter.lock().unwrap();
        *c += 1;
    }));
}
for h in handles { h.join().unwrap(); }
println!("{}", counter.lock().unwrap());   // 10

Arc<Mutex<T>> is the canonical "shared mutable state across threads" pattern. lock() returns a MutexGuard that auto-unlocks when dropped (RAII).

Channels

For message-passing concurrency:

use std::sync::mpsc;
use std::thread;

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

for i in 0..5 {
    let tx = tx.clone();
    thread::spawn(move || {
        tx.send(i * i).unwrap();
    });
}
drop(tx);   // close the original sender

for v in rx {
    println!("got: {}", v);
}

Receiver iteration ends when all senders are dropped.

Common mistakes

  • Forgetting move when the closure needs to outlive the spawner — compile error guides you.
  • Sharing Rc<T> across threads — won't compile. Use Arc.
  • Holding a Mutex during a long operation — blocks other threads. Lock briefly.
  • Spawning many threads without bounding — use a thread pool (rayon, tokio) for production work.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…