Threads

#279 Jul 2026

279. thread::park — Block a Thread Without Dragging In a Mutex

This morning’s bite (278) needed a Mutex + Condvar pair just to make one thread wait for another. When exactly one thread sleeps and you hold its handle, thread::park does it with no lock at all.

Every thread owns a park token. thread::park() blocks until the token is available, then consumes it; unpark() makes it available. That token is the whole trick:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::thread;
use std::time::Duration;

let h = thread::spawn(|| {
    thread::sleep(Duration::from_millis(50));
    thread::park(); // returns instantly — token already there
});

h.thread().unpark(); // fires BEFORE the park
h.join().unwrap();

With a naive flag-and-sleep scheme, waking a thread before it goes to sleep means the wakeup is lost. The token can’t be lost: an early unpark is banked, and the next park returns immediately. That’s the race Condvar needs its Mutex to prevent — park solves it for free.

One Condvar lesson still applies, though: park may also wake spuriously, so pair it with a flag and re-check in a loop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;

let go = Arc::new(AtomicBool::new(false));
let go2 = Arc::clone(&go);

let worker = thread::spawn(move || {
    while !go2.load(Ordering::Acquire) {
        thread::park(); // sleep until unparked
    }
    // ... do the work
});

go.store(true, Ordering::Release);
worker.thread().unpark();
worker.join().unwrap();

The flag decides whether to run; park only decides when to stop burning CPU. Store with Release, load with Acquire, and the write is visible when the loop exits — the exact handoff from bite 277.

Note what you didn’t need: no Mutex, no guard rebinding, no Condvar. The waiter is addressed directly through its Thread handle (worker.thread() — cloneable, Send, happy to be stashed anywhere).

There’s a deadline-aware sibling, thread::park_timeout(dur), for “wake me up when signaled, or after 10ms, whichever comes first” — the classic backoff loop in lock-free code.

Reach for Condvar when many threads wait on shared state; reach for park when one known thread naps until another taps it. It’s what mpsc channels and most executors use under the hood.

Stable since Rust 1.0.

40. Scoped Threads — Borrow Across Threads Without Arc

Need to share stack data with spawned threads? std::thread::scope lets you borrow local variables across threads — no Arc, no .clone().

The problem

With std::thread::spawn, you can’t borrow local data because the thread might outlive the data:

1
2
3
4
5
6
7
let data = vec![1, 2, 3];

// This won't compile — `data` might be dropped
// while the thread is still running
// std::thread::spawn(|| {
//     println!("{:?}", data);
// });

The classic workaround is wrapping everything in Arc:

1
2
3
4
5
6
7
8
9
use std::sync::Arc;

let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);

let handle = std::thread::spawn(move || {
    println!("{:?}", data_clone);
});
handle.join().unwrap();

It works, but it’s noisy — especially when you just want to read some data in parallel.

The fix: std::thread::scope

Scoped threads guarantee that all spawned threads finish before the scope exits, so borrowing is safe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let data = vec![1, 2, 3];
let mut results = vec![];

std::thread::scope(|s| {
    s.spawn(|| {
        // Borrowing `data` directly — no Arc needed
        println!("Thread sees: {:?}", data);
    });

    s.spawn(|| {
        let sum: i32 = data.iter().sum();
        println!("Sum: {sum}");
    });
});

// All threads have joined here — guaranteed
println!("Done! data is still ours: {:?}", data);

Mutable access works too

Since the scope enforces proper lifetimes, you can even have one thread mutably borrow something:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut counts = [0u32; 3];

std::thread::scope(|s| {
    for (i, count) in counts.iter_mut().enumerate() {
        s.spawn(move || {
            *count = (i as u32 + 1) * 10;
        });
    }
});

assert_eq!(counts, [10, 20, 30]);

Each thread gets exclusive access to its own element — the borrow checker is happy, no Mutex required.

When to reach for scoped threads

Use std::thread::scope when you need parallel work on local data and don’t want the overhead or ceremony of Arc/Mutex. It’s perfect for fork-join parallelism: spin up threads, borrow what you need, collect results when they’re done.