Loading page…
Rust walkthroughs
Loading page…
A JoinHandle is returned by thread::spawn() and represents a handle to a spawned thread. It allows you to wait for the thread to complete and retrieve its return value. JoinHandle is essential for managing thread lifecycles and collecting results.
Key characteristics:
join() returns Result<T, Box<dyn Any>> containing the thread's return valuejoin() returns Err with the panic payloadthread().id()The JoinHandle<T> type is the primary way to interact with spawned threads after creation. It implements Send and Sync, so handles can be shared between threads.
use std::thread;
use std::time::Duration;
fn main() {
// spawn returns a JoinHandle<T>
let handle: thread::JoinHandle<i32> = thread::spawn(|| {
println!("Thread working...");
thread::sleep(Duration::from_millis(100));
42 // Return value
});
// Do other work while thread runs
println!("Main thread doing other work");
// join() blocks until thread completes, returns Result<T, ...>
let result = handle.join().unwrap();
println!("Thread returned: {}", result);
}use std::thread;
fn main() {
let handle = thread::spawn(|| {
panic!("Something went wrong in the thread!");
});
// join() returns Err if thread panicked
match handle.join() {
Ok(_) => println!("Thread completed successfully"),
Err(e) => {
// e is Box<dyn Any + Send>
if let Some(msg) = e.downcast_ref::<&str>() {
println!("Thread panicked: {}", msg);
} else if let Some(msg) = e.downcast_ref::<String>() {
println!("Thread panicked: {}", msg);
} else {
println!("Thread panicked with unknown type");
}
}
}
}use std::thread;
fn main() {
let handles: Vec<thread::JoinHandle<i32>> = (0..5)
.map(|i| {
thread::spawn(move || {
// Simulate computation
let result = i * i;
println!("Thread {} computed {}", i, result);
result
})
})
.collect();
// Collect all results
let results: Vec<i32> = handles
.into_iter()
.map(|h| h.join().unwrap())
.collect();
println!("All results: {:?}", results);
println!("Sum: {}", results.iter().sum::<i32>());
}use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
thread::sleep(Duration::from_millis(500));
println!("Thread completed");
});
// is_finished() checks if thread has terminated (unstable - use join_timeout pattern instead)
// For stable Rust, we use try_join or timing patterns
println!("Waiting for thread...");
// We can check periodically using non-blocking patterns
// Standard JoinHandle doesn't have is_finished() in stable
// Alternative: use a channel to signal completion
handle.join().unwrap();
println!("Thread has finished");
}use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Inside spawned thread");
thread::current().id()
});
// Access thread ID before joining
let thread_id = handle.thread().id();
println!("Spawned thread ID: {:?}", thread_id);
// Join and get the return value
let inner_id = handle.join().unwrap();
println!("Thread returned its ID: {:?}", inner_id);
}use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let handle = thread::spawn(move || {
thread::sleep(Duration::from_secs(2));
tx.send(42).unwrap();
});
// Wait with timeout using channel
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(value) => println!("Got value: {}", value),
Err(_) => {
println!("Timeout - thread still running");
// We can still join later
handle.join().unwrap();
}
}
}use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 0..3 {
println!("Fire and forget: {}", i);
thread::sleep(Duration::from_millis(100));
}
});
// thread::spawn returns JoinHandle
// If we don't care about the result, we can "forget" it
// The thread will still run to completion
// But dropping without joining will block until completion!
// To truly fire-and-forget without blocking on drop:
handle.join().unwrap(); // Must explicitly join if we want non-blocking behavior
// Alternative: use thread::spawn with detached semantics (requires crate)
// Or just don't store the handle and let it drop (will block!)
}use std::thread;
fn spawn_worker(id: u32) -> thread::JoinHandle<Result<String, String>> {
thread::spawn(move || {
if id == 3 {
Err(format!("Worker {} failed", id))
} else {
Ok(format!("Worker {} succeeded", id))
}
})
}
fn main() {
let handles: Vec<_> = (0..5).map(spawn_worker).collect();
let mut successes = Vec::new();
let mut failures = Vec::new();
for (id, handle) in handles.into_iter().enumerate() {
match handle.join() {
Ok(Ok(msg)) => successes.push(msg),
Ok(Err(e)) => failures.push(e),
Err(panic) => {
if let Some(msg) = panic.downcast_ref::<&str>() {
failures.push(format!("Panic: {}", msg));
} else {
failures.push("Unknown panic".to_string());
}
}
}
}
println!("Successes: {:?}", successes);
println!("Failures: {:?}", failures);
}use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
// JoinHandle is Send + Sync, so can be shared
let handles = Arc::new(Mutex::new(Vec::<thread::JoinHandle<i32>>::new()));
let h1 = Arc::clone(&handles);
let producer1 = thread::spawn(move || {
let handle = thread::spawn(|| 10);
h1.lock().unwrap().push(handle);
});
let h2 = Arc::clone(&handles);
let producer2 = thread::spawn(move || {
let handle = thread::spawn(|| 20);
h2.lock().unwrap().push(handle);
});
producer1.join().unwrap();
producer2.join().unwrap();
// Now join all stored handles
let handles = Arc::try_unwrap(handles)
.expect("All references dropped")
.into_inner()
.unwrap();
for handle in handles {
println!("Result: {}", handle.join().unwrap());
}
}use std::thread;
use std::sync::Arc;
fn main() {
let data = vec![1, 2, 3, 4, 5];
// Regular JoinHandle requires 'static data
println!("=== Regular JoinHandle ===");
{
let data = Arc::new(data.clone());
let handles: Vec<_> = (0..5)
.map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || data[i] * 2)
})
.collect();
for handle in handles {
println!("Result: {}", handle.join().unwrap());
}
}
// Scoped threads can borrow
println!("\n=== Scoped Thread ===");
{
thread::scope(|s| {
let handles: Vec<_> = (0..5)
.map(|i| s.spawn(move || data[i] * 2))
.collect();
for handle in handles {
println!("Result: {}", handle.join().unwrap());
}
});
}
}use std::thread;
use std::sync::{mpsc, Arc, Mutex};
type Job = Box<dyn FnOnce() + Send + 'static>;
struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
struct Worker {
handle: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Self {
let handle = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv();
match job {
Ok(job) => job(),
Err(_) => break, // Channel closed
}
}
});
Self { handle: Some(handle) }
}
}
impl ThreadPool {
fn new(size: usize) -> Self {
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let workers = (0..size)
.map(|_| Worker::new(Arc::clone(&receiver)))
.collect();
Self { sender: Some(sender), workers }
}
fn execute<F: FnOnce() + Send + 'static>(&self, f: F) {
self.sender.as_ref().unwrap().send(Box::new(f)).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
// Close channel
drop(self.sender.take());
// Wait for all workers to finish
for worker in &mut self.workers {
if let Some(handle) = worker.handle.take() {
handle.join().unwrap();
}
}
}
}
fn main() {
let pool = ThreadPool::new(4);
for i in 0..8 {
pool.execute(move || {
println!("Task {} running on thread {:?}",
i, thread::current().id());
});
}
// Pool drops and joins all workers
}use std::thread;
use std::sync::{Arc, Mutex};
use std::time::Duration;
struct AsyncResult<T> {
handle: thread::JoinHandle<T>,
completed: Arc<Mutex<bool>>,
}
impl<T: Send + 'static> AsyncResult<T> {
fn new<F>(f: F) -> Self
where
F: FnOnce() -> T + Send + 'static,
{
let completed = Arc::new(Mutex::new(false));
let completed_clone = Arc::clone(&completed);
let handle = thread::spawn(move || {
let result = f();
*completed_clone.lock().unwrap() = true;
result
});
Self { handle, completed }
}
fn is_completed(&self) -> bool {
*self.completed.lock().unwrap()
}
fn wait(self) -> T {
self.handle.join().unwrap()
}
}
fn main() {
let result = AsyncResult::new(|| {
thread::sleep(Duration::from_secs(1));
42
});
while !result.is_completed() {
println!("Still computing...");
thread::sleep(Duration::from_millis(200));
}
println!("Result: {}", result.wait());
}use std::thread;
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use std::time::Duration;
fn main() {
let cancelled = Arc::new(AtomicBool::new(false));
let cancelled_clone = Arc::clone(&cancelled);
let handle = thread::spawn(move || {
for i in 0..100 {
if cancelled_clone.load(Ordering::Relaxed) {
println!("Thread cancelled at iteration {}", i);
return "cancelled";
}
thread::sleep(Duration::from_millis(50));
}
"completed"
});
// Simulate user cancellation
thread::sleep(Duration::from_millis(200));
cancelled.store(true, Ordering::Relaxed);
let result = handle.join().unwrap();
println!("Thread result: {}", result);
}| Method | Description |
|--------|-------------|
| join() | Block until thread completes, return Result<T, Box<dyn Any>> |
| thread() | Get reference to the underlying Thread |
| thread().id() | Get the thread's ID |
JoinHandle Lifecycle:
spawn() → Running → Completed/panicked
↓ ↓ ↓
JoinHandle JoinHandle join() returns Ok/Err
Common Patterns:
| Pattern | Description |
|---------|-------------|
| Collect handles | Store handles in a Vec, join all later |
| Check result | handle.join() returns thread's value |
| Handle panic | Check join() Err for panic payload |
| Fire-and-forget | Drop handle (blocks) or join explicitly |
Key Points:
join() blocks the calling thread until the spawned thread finishesErr from join()downcast_ref() to extract panic messageSend and Sync