Introduction
Rust's std::thread module spawns OS threads that run independently. Unlike some languages, Rust does not provide a built-in is_finished() method on JoinHandle (until Rust 1.61+). The primary mechanism is join(), which blocks until the thread completes. For non-blocking checks, you can use JoinHandle::is_finished() (stabilized in Rust 1.61), channels, atomic flags, or Arc<Mutex<bool>>.
Method 1: join() — Blocking Wait
join() blocks the calling thread until the spawned thread finishes:
1use std::thread;
2use std::time::Duration;
3
4fn main() {
5 let handle = thread::spawn(|| {
6 thread::sleep(Duration::from_secs(2));
7 println!("Thread finished work");
8 42
9 });
10
11 // Blocks until the thread completes
12 let result = handle.join().unwrap();
13 println!("Thread returned: {}", result); // 42
14}
join() returns Result<T, Box<dyn Any>>. If the thread panicked, join() returns Err containing the panic payload:
1let handle = thread::spawn(|| {
2 panic!("Something went wrong!");
3});
4
5match handle.join() {
6 Ok(val) => println!("Success: {:?}", val),
7 Err(e) => println!("Thread panicked: {:?}", e),
8}
Method 2: is_finished() — Non-Blocking Check (Rust 1.61+)
JoinHandle::is_finished() returns true if the thread has completed:
1use std::thread;
2use std::time::Duration;
3
4fn main() {
5 let handle = thread::spawn(|| {
6 thread::sleep(Duration::from_secs(2));
7 "done"
8 });
9
10 // Poll without blocking
11 loop {
12 if handle.is_finished() {
13 println!("Thread is done!");
14 let result = handle.join().unwrap();
15 println!("Result: {}", result);
16 break;
17 }
18 println!("Thread still running...");
19 thread::sleep(Duration::from_millis(500));
20 }
21}
This is the simplest non-blocking approach, but polling in a loop is wasteful. Prefer channels or condition variables for event-driven notification.
Method 3: Channels for Completion Notification
Use mpsc::channel to receive a signal when the thread finishes:
1use std::sync::mpsc;
2use std::thread;
3use std::time::Duration;
4
5fn main() {
6 let (tx, rx) = mpsc::channel();
7
8 thread::spawn(move || {
9 // Do work
10 thread::sleep(Duration::from_secs(2));
11 let result = 42;
12 tx.send(result).unwrap();
13 });
14
15 // Non-blocking check
16 match rx.try_recv() {
17 Ok(result) => println!("Thread done: {}", result),
18 Err(mpsc::TryRecvError::Empty) => println!("Still running"),
19 Err(mpsc::TryRecvError::Disconnected) => println!("Thread dropped sender"),
20 }
21
22 // Blocking wait with timeout
23 match rx.recv_timeout(Duration::from_secs(5)) {
24 Ok(result) => println!("Got result: {}", result),
25 Err(mpsc::RecvTimeoutError::Timeout) => println!("Timed out"),
26 Err(mpsc::RecvTimeoutError::Disconnected) => println!("Channel closed"),
27 }
28}
Method 4: Shared Atomic Flag
Use AtomicBool for a lightweight completion flag:
1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::thread;
4use std::time::Duration;
5
6fn main() {
7 let finished = Arc::new(AtomicBool::new(false));
8 let finished_clone = Arc::clone(&finished);
9
10 let handle = thread::spawn(move || {
11 thread::sleep(Duration::from_secs(2));
12 // Signal completion
13 finished_clone.store(true, Ordering::Release);
14 });
15
16 // Check from main thread
17 while !finished.load(Ordering::Acquire) {
18 println!("Waiting...");
19 thread::sleep(Duration::from_millis(500));
20 }
21
22 println!("Thread finished!");
23 handle.join().unwrap();
24}
Method 5: Condvar for Efficient Waiting
Use a condition variable to wait without busy-polling:
1use std::sync::{Arc, Condvar, Mutex};
2use std::thread;
3use std::time::Duration;
4
5fn main() {
6 let pair = Arc::new((Mutex::new(false), Condvar::new()));
7 let pair_clone = Arc::clone(&pair);
8
9 thread::spawn(move || {
10 thread::sleep(Duration::from_secs(2));
11
12 let (lock, cvar) = &*pair_clone;
13 let mut finished = lock.lock().unwrap();
14 *finished = true;
15 cvar.notify_one();
16 });
17
18 // Wait efficiently (no polling)
19 let (lock, cvar) = &*pair;
20 let mut finished = lock.lock().unwrap();
21 while !*finished {
22 // Wait with timeout
23 let result = cvar.wait_timeout(finished, Duration::from_secs(5)).unwrap();
24 finished = result.0;
25 if result.1.timed_out() {
26 println!("Timed out waiting for thread");
27 break;
28 }
29 }
30
31 if *finished {
32 println!("Thread completed!");
33 }
34}
Managing Multiple Threads
1use std::thread;
2use std::time::Duration;
3
4fn main() {
5 let handles: Vec<_> = (0..5).map(|i| {
6 thread::spawn(move || {
7 thread::sleep(Duration::from_millis(i * 500));
8 println!("Thread {} done", i);
9 i * 10
10 })
11 }).collect();
12
13 // Wait for all threads
14 let results: Vec<_> = handles
15 .into_iter()
16 .map(|h| h.join().unwrap())
17 .collect();
18
19 println!("All results: {:?}", results); // [0, 10, 20, 30, 40]
20}
Comparison of Methods
| Method | Blocking | Notification | Complexity |
join() | Yes | On completion | Simplest |
is_finished() | No (poll) | Polling | Simple, Rust 1.61+ |
mpsc::channel | Optional | Event-driven | Moderate |
AtomicBool | No (poll) | Polling | Low overhead |
Condvar | Efficient wait | Event-driven | Most flexible |
Common Pitfalls
Forgetting to join(): If you drop a JoinHandle without calling join(), the thread is detached and continues running. The program may exit before the thread finishes, losing its work.
Panics in threads: A panic in a spawned thread does not crash the main thread. It is silently contained until join() is called, which returns Err. Always handle the Result from join().
Busy-polling waste: Using is_finished() or AtomicBool in a tight loop without thread::sleep() burns CPU. Use condition variables or channels for efficient waiting.
Ordering on atomics: Use Ordering::Release when writing and Ordering::Acquire when reading the flag. Relaxed ordering may cause the reader to see stale values on some architectures.
Deadlocks with Mutex: If the thread holds a Mutex lock when it panics, the Mutex becomes poisoned. Subsequent lock() calls return Err. Use lock().unwrap_or_else(|e| e.into_inner()) to recover from poisoned mutexes if appropriate.
Summary
Use handle.join() to block until a thread completes — simplest and most common
Use handle.is_finished() (Rust 1.61+) for non-blocking polling
Use mpsc::channel with try_recv() for event-driven completion notification
Use Condvar for efficient waiting without busy-polling
Always call join() to collect the result and handle potential panics — dropping a JoinHandle detaches the thread