A thread is the smallest executable unit of a process.
Threads allow us to split the computation in our program into multiple threads. Running multiple tasks at the same time can improve performance of the code. However, it can add complexity.
Creating a New Thread in Rust
In Rust, we can create a native operating system thread using the thread::spawn()
function from the std
module. The spawn method takes a closure as an argument.
Here is the syntax of thread::spawn()
,
thread::spawn(|| {
// code to execute in the thread
})
Now, let's see an example.
use std::thread;
use std::time::Duration;
fn main() {
// create a thread
thread::spawn(|| {
// everything in here runs in a separate thread
for i in 0..10 {
println!("{} from the spawned thread!", i);
thread::sleep(Duration::from_millis(2));
}
});
// main thread
for i in 0..5 {
println!("{} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
Output
0 from the main thread! 0 from the spawned thread! 1 from the main thread! 1 from the spawned thread! 2 from the main thread! 3 from the main thread! 2 from the spawned thread! 4 from the main thread!
In the example above, we create a thread using the thread::spawn()
function. The thread loops over 0..5
and prints the current value.
Similarly, we have a main thread where we loop over 0..5
and print the current value.
We also call thread::sleep
to force a thread to stop its execution for a short duration, allowing a different thread to run.
Notice that we sleep 2 milliseconds in the spawned thread and 1 millisecond in the main thread.
The output from this program might be a little different every time. The important thing to remember here is that if the main thread completes, all other threads are shut down whether or not they have finished running.
So, even though the spawned thread should print until i
is 9, it only reaches to 2 because the main thread shut down.
Join Handles in Rust
A spawned thread always returns a join handle. If we want the spawned thread to complete execution, we can save the return value of thread::spawn
in a variable and then call the join()
method on it.
The join()
method on JoinHandle
(return type of thread::spawn
) waits for the spawned thread to finish.
Let's look at an example.
use std::thread;
use std::time::Duration;
fn main() {
// create a thread and save the handle to a variable
let handle = thread::spawn(|| {
// everything in here runs in a separate thread
for i in 0..10 {
println!("{} from the spawned thread!", i);
thread::sleep(Duration::from_millis(2));
}
});
// main thread
for i in 0..5 {
println!("{} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
// wait for the separate thread to complete
handle.join().unwrap();
}
Output
0 from the main thread! 0 from the spawned thread! 1 from the main thread! 2 from the main thread! 1 from the spawned thread! 3 from the main thread! 2 from the spawned thread! 4 from the main thread! 3 from the spawned thread! 4 from the spawned thread! 5 from the spawned thread! 6 from the spawned thread! 7 from the spawned thread! 8 from the spawned thread! 9 from the spawned thread!
Here, we save the return of the thread::spawn()
function and bind it to a variable called handle
.
In the final line of the code, we call the join()
method of the handle
. Calling join()
on the handle
blocks the thread until the thread terminates.
The two threads (main and spawned thread) continue alternating for some time, but the main thread waits because of handle.join()
and does not end until the spawned thread is finished.
If we move the handle.join()
before the final loop, the output will change and the print statements won't be interleaved.
use std::thread;
use std::time::Duration;
fn main() {
// create a thread and save the handle to a variable
let handle = thread::spawn(|| {
// everything in here runs in a separate thread
for i in 0..10 {
println!("{} from the spawned thread!", i);
thread::sleep(Duration::from_millis(2));
}
});
// wait for the separate thread to complete
handle.join().unwrap();
// main thread
for i in 0..5 {
println!("{} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
Output
0 from the spawned thread! 1 from the spawned thread! 2 from the spawned thread! 3 from the spawned thread! 4 from the spawned thread! 5 from the spawned thread! 6 from the spawned thread! 7 from the spawned thread! 8 from the spawned thread! 9 from the spawned thread! 0 from the main thread! 1 from the main thread! 2 from the main thread! 3 from the main thread! 4 from the main thread!
Thus, it is important to know where join()
is called. I will dictate whether threads run at the same time or not.
Using move Closures with Threads in Rust
A value can be moved into a separate thread by passing it as an argument to the thread::spawn()
function.
Let's look at an example.
use std::thread;
fn main() {
// main thread starts here
let message = String::from("Hello, World!");
// move the message value to a separate thread
let handle = thread::spawn(move || {
println!("{}", message);
});
// wait for the thread to finish
handle.join().unwrap();
}
Output
Hello, World!
Here, the closure ||
passed to the thread::spawn()
function uses the move
keyword to indicate that it takes ownership of the message
variable.
When a value is moved into a thread, ownership of the value is transferred to the thread, and the main thread can no longer access the value.
This means that the closure can use the message
variable even after the main thread has completed.
Let's look at what happens if we don't use the move
keyword in front of the closure.
use std::thread;
fn main() {
let message = String::from("Hello, World!");
// using the message variable without a move
let handle = thread::spawn(|| {
println!("{}", message);
});
handle.join().unwrap();
}
Output
error[E0373]: closure may outlive the current function, but it borrows `message`, which is owned by the current function --> src/main.rs:7:32 | 7 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `message` 8 | println!("{}", message); | ------- `message` is borrowed here |
The program in this case fails to compile. Here, Rust will try to borrow the message
variable into the separate thread.
7 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `message`
However, Rust doesn't know how long the spawned thread will run. Thus it can't tell if the reference to the message
variable will always be valid.
By adding the move
keyword before the closure, we force the closure to take ownership of the message
variable or any variable used inside closure.
We are telling Rust that the main thread won't use the message
variable anymore. This is a classic example of Rust ownership and how it saves us from mishaps. To learn more about ownership in Rust, visit Rust Ownership.
Note that moving a value into a thread can be useful for parallelism, but it can also be a source of bugs if not used carefully.
Sending Messages between Threads in Rust
In Rust, threads can communicate with each other by sending messages through channels. A channel is a way to send values between threads, and it can be used to synchronize communication and avoid data races.
We use the channel()
function in the std::sync::mspsc
module to create a channel in Rust.
Let's take a look at how we can use channels to communicate between threads.
use std::sync::mpsc;
use std::thread;
fn main() {
// main thread starts here
// create a new channel
let (sender, receiver) = mpsc::channel();
// spawn a new thread
let handle = thread::spawn(move || {
// receive message from channel
let message = receiver.recv().unwrap();
println!("Received message: {}", message);
});
let message = String::from("Hello, World!");
// send message to channel
sender.send(message).unwrap();
// wait for spawned thread to finish
handle.join().unwrap();
}
Output
Received message: Hello, World!
Here, we create a channel using the channel()
function. The std::sync::mpsc
module provides multiple-producer, single-consumer (mspc) channels that can be used to send values between threads.
// create a new channel
let (sender, receiver) = mpsc::channel();
The sender
and receiver
variables represent the two endpoints of the channel. The sender endpoint is used to send messages, while the receiver endpoint is used to receive messages.
// spawn a new thread
let handle = thread::spawn(move || {
// receive message from channel
let message = receiver.recv().unwrap();
println!("Received message: {}", message);
});
We also create a spawned thread using the thread::spawn()
function. The closure passed to the function receives a message using the receiver.recv()
method.
The recv()
method blocks until a message is received on the channel, and it returns a Result
indicating whether a message was received or an error occurred.
let message = String::from("Hello, World!");
// send message to channel
sender.send(message).unwrap();
In the main thread, a message
is created and sent using the sender.send()
method. The send(
) method returns a Result
indicating whether the message was successfully sent or an error occurred.
// wait for spawned thread to finish
handle.join().unwrap();
Finally, the join()
method is called on the handle to wait for the spawned thread to finish before the program exits.