Threads

Thread Handles

A thread handle in Rust is represented by two distinct types in std::thread:

  1. JoinHandle<T> (and ScopedJoinHandle<'scope, T>): The unique owning handle returned when spawning a thread, used to wait for the thread to finish (.join()) and retrieve its return value T.
  2. Thread: A cloneable metadata and wakeup descriptor (accessible via handle.thread() or std::thread::current()) that identifies a thread's ThreadId, optional name, and .unpark() token.
Unique Join Handle (`JoinHandle<T>` — Not Clone!)
  +-- .join() -> thread::Result<T>   (Consumes JoinHandle, waits for completion)
  +-- .is_finished() -> bool         (Non-blocking completion check)
  +-- .thread() -> &Thread           (Borrows the underlying Thread descriptor)
                     |
                     v
Cloneable Thread Descriptor (`Thread` — Implements Clone, Send, Sync!)
  +-- .id() -> ThreadId
  +-- .name() -> Option<&str>
  +-- .unpark()                      (Wakes up the thread if parked via `thread::park()`)

Dropping a JoinHandle<T> Detaches the Thread

Unlike a mutex guard or file handle, dropping a JoinHandle<T> does NOT kill or cancel the running OS thread. Instead, dropping a JoinHandle<T> detaches the thread: the thread continues running in the background until its closure finishes (or the main process exits), and the OS automatically reclaims its thread resources upon completion.

Action on JoinHandle<T> Effect on Background Thread Return Value T
let res = handle.join(); Blocks caller until background thread finishes Returned inside Ok(T) (or Err(panic_payload))
drop(handle); (or ignoring thread::spawn(...)) Detaches thread; continues running in background Dropped inside the background thread when it finishes
handle.is_finished() Non-blocking poll (true if thread has exited) Still held inside handle until .join() is called

Polling Completion with JoinHandle::is_finished and Parking with Thread::unpark

Stabilized in Rust 1.61, handle.is_finished() checks whether the associated thread has terminated without blocking. In addition, handle.thread().unpark() pairs with std::thread::park() to provide a lightweight per-thread wakeup mechanism:

use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;

static READY: AtomicBool = AtomicBool::new(false);

fn main() {
    let worker = thread::Builder::new()
        .name(String::from("parked-worker"))
        .spawn(|| {
            // Park in a loop checking the atomic condition (guards against spurious wakeups):
            while !READY.load(Ordering::Acquire) {
                thread::park();
            }
            println!(
                "[{:?}] woke up and saw READY == true!",
                thread::current().name().unwrap_or("unnamed")
            );
            99_u32
        })
        .unwrap();

    println!("worker is_finished before unpark: {}", worker.is_finished());

    // Set condition and wake the worker via its `Thread` descriptor:
    READY.store(true, Ordering::Release);
    worker.thread().unpark();

    let output = worker.join().unwrap();
    println!("worker joined with output: {}", output);
}
worker is_finished before unpark: false
["parked-worker"] woke up and saw READY == true!
worker joined with output: 99