Rust
threads
concurrency
programming
thread-completion

How to check if a thread has finished in Rust?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.