Concurrency is the ability of a program to have multiple computations happening at the same time. Rust’s ownership and type system are designed to manage many of the common bugs found in concurrent programming, allowing for what is often called “fearless concurrency.”
The most fundamental way to run code concurrently is by creating a new thread. Rust’s standard library provides the std::thread module for working with native OS threads.
Creating a New Thread with thread::spawn
You can create a new thread using the std::thread::spawn function. It takes a closure—an anonymous function—that contains the code you want to run in the new thread.
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
When you run this code, you’ll likely see the output from both threads interleaved. However, you might also notice that the spawned thread stops prematurely. This is because the main thread finishes before the spawned thread has a chance to complete its loop. When the main thread exits, the entire program shuts down, terminating all other threads.
Waiting for Threads to Finish with JoinHandle
To ensure a spawned thread finishes before the main thread exits, you need to wait for it. The thread::spawn function returns a JoinHandle. A JoinHandle is an owned value that you can call the join method on. Calling join will block the currently running thread until the thread associated with the handle terminates.
Let’s fix our previous example by storing the JoinHandle and calling join on it.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
// Block the main thread until the spawned thread finishes.
handle.join().unwrap();
}
Now, the main thread will wait for the spawned thread to finish its work before exiting, and you will see all the output from the spawned thread. The join method returns a Result, which we unwrap() here for simplicity.
Using move Closures with Threads
It’s common to want to use data from the main thread within a spawned thread. However, Rust’s ownership rules come into play. The compiler needs to ensure that any data referenced by the thread will be valid for as long as the thread runs.
Consider this example, which tries to print a vector from a spawned thread:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// This will not compile!
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
The compiler will give an error because it cannot be sure how long the spawned thread will run. It’s possible the main function could end and drop v before the thread is done. To fix this, we need to give ownership of v to the spawned thread. We can do this by adding the move keyword before the closure.
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// Use `move` to force the closure to take ownership of `v`.
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
// We can no longer use `v` in the main thread because it has been moved.
// drop(v); // This would cause a compile error.
handle.join().unwrap();
}
The move keyword forces the closure to take ownership of the values it uses from its environment. In this case, it moves v into the spawned thread, guaranteeing its validity for the lifetime of the thread.
Conclusion
Creating threads with std::thread::spawn is the first step into concurrent programming in Rust. By using JoinHandle to wait for threads and the move keyword to transfer ownership of data, you can write safe, basic concurrent programs. For more complex scenarios involving communication between threads or sharing state, you’ll need to explore other tools like channels (mpsc) and thread-safe smart pointers like Mutex and Arc.