std::thread::spawn creates a genuine OS thread running a closure concurrently with the caller. Rust's ownership rules extend naturally to threads: moving data into a thread with the move keyword transfers ownership, so the original thread can no longer access it, and Send/Sync marker traits let the compiler reject at compile time any type that isn't actually safe to share or transfer across threads. This is the basis of what the Rust community calls "fearless concurrency" — most data races are caught before the program runs, not discovered later by testing or in production.
Spawning a thread
thread::spawn returns a JoinHandle; calling .join() on it blocks the current thread until the spawned one finishes.
use std::thread;
let handle = thread::spawn(|| {
for i in 1..5 {
println!("spawned thread: {}", i);
}
});
for i in 1..3 {
println!("main thread: {}", i);
}
handle.join().unwrap(); // wait for the spawned thread to finishThe compiler enforces safe data sharing
A closure that captures a variable and is sent to a thread must use move, transferring ownership — without it, the compiler cannot guarantee the captured data outlives the thread, and refuses to compile.
let data = vec![1, 2, 3];
let handle = thread::spawn(move || { // move required: ownership transfers to the thread
println!("{:?}", data);
});
handle.join().unwrap();
// println!("{:?}", data); // COMPILE ERROR: data was moved into the threadChannels: message passing between threads
std::sync::mpsc ("multiple producer, single consumer") provides channels for threads to communicate by sending owned values, rather than sharing mutable memory directly.
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("hello from the thread").unwrap();
});
let received = rx.recv().unwrap(); // blocks until a message arrives
println!("{}", received);