Rust by Example: Threads Basics
Enter the world of concurrent programming. Learn how to spawn OS-level threads using std::thread, pass data to them using move closures, and synchronize their execution using join handles.
Code
use std::thread;
use std::time::Duration;
fn main() {
// Spawn a new thread
// The closure passed to spawn is the code the thread will run
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
// Code on the main thread
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
// Wait for the spawned thread to finish
// If we don't call join(), the main thread might exit
// before the spawned thread finishes.
handle.join().unwrap();
// Using 'move' closures with threads
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}Explanation
Rust provides a 1:1 threading model, meaning each language thread corresponds directly to one operating system thread. This allows you to leverage the full power of your CPU cores and OS scheduling, but it comes with a higher context-switching cost compared to "green threads" (like Go routines). The std::thread module contains the essential utilities for creating and managing these threads.
To create a thread, you use thread::spawn, passing it a closure with the code to run. This function returns a JoinHandle. Crucially, if the main thread completes, the entire process exits, potentially killing other running threads prematurely. To prevent this, you must call join() on the handle, which blocks the current thread until the spawned thread finishes.
Thread safety in Rust is enforced by the type system via the Send and Sync traits. When passing data to a thread, you often need the move keyword. This forces the closure to take ownership of the captured values, ensuring they are moved from the main thread to the new thread. This prevents "use-after-free" errors where the main thread might drop data while the spawned thread is still trying to use it.
Code Breakdown
thread::spawn(|| { ... }). This spawns a new OS thread. The code inside the closure runs concurrently with the main thread. The return value is a handle that owns the thread resource.thread::sleep(...). Puts the current thread to sleep for the specified duration, allowing the OS scheduler to run other threads.handle.join().unwrap(). join() blocks until the thread terminates. It returns a Result containing the thread's return value or an error if the thread panicked. We unwrap it to assert success.move ||. The move keyword forces the closure to take ownership of v. Without it, the closure would try to borrow v, but the compiler would reject it because it can't guarantee v lives long enough.
