Rust
threads
concurrency
programming
thread-completion

How to check if a thread has finished in Rust?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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:

rust
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:

rust
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:

rust
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:

rust
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:

rust
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:

rust
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

rust
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

MethodBlockingNotificationComplexity
join()YesOn completionSimplest
is_finished()No (poll)PollingSimple, Rust 1.61+
mpsc::channelOptionalEvent-drivenModerate
AtomicBoolNo (poll)PollingLow overhead
CondvarEfficient waitEvent-drivenMost 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

Course illustration
Course illustration

All Rights Reserved.