16.1.1 What Is Concurrency?
- Concurrent means different parts of a program run independently.
- Parallel means different parts of a program run at the same time.
The Rust Programming Language describes Rust’s support for concurrency with the phrase:
Fearless concurrency
Handling concurrent programming safely and efficiently is another major goal of Rust. As more and more computers use multiple processors, concurrent programming, in which different parts of a program execute independently, and parallel programming, in which different parts of a program execute simultaneously, have become increasingly important. Historically, programming in these environments has been difficult and error-prone—Rust aims to change that.
At first, the Rust team thought that ensuring memory safety and preventing concurrency problems were two separate challenges that required different solutions. Over time, the team discovered that ownership and the type system are a powerful set of tools that help manage both memory safety and concurrency problems!
By leveraging ownership and type checking, many concurrency errors are compile-time errors in Rust rather than runtime errors. As a result, incorrect code is rejected and an explanatory error is shown instead of making you spend a long time trying to reproduce the exact runtime concurrency failure. You can therefore fix the code while you are still working on it, instead of after it has already shipped to production.
We call this aspect of Rust “fearless concurrency.” Fearless concurrency lets you write code with no subtle bugs and refactor it easily without introducing new ones.
The most important sentence is this: Fearless concurrency lets you write code with no subtle bugs and refactor it easily without introducing new ones.
Note: in this chapter, “concurrency” is used as a general term covering both concurrent and parallel execution.
16.1.2 Processes and Threads
In most modern operating systems, code runs in processes, and the system manages multiple processes at the same time. Inside your program, the independent parts that can run simultaneously are called threads.
Because multiple threads can run at the same time, we often split a program’s work into multiple threads so that they can run concurrently. This has both advantages and disadvantages:
- It improves performance.
- It increases complexity: the order in which threads execute cannot be guaranteed.
16.1.3 Problems Caused by Multithreading
- Race condition: threads access data or resources in an inconsistent order.
- Deadlock: two threads wait for each other to finish using the resources they hold, so neither can continue.
- Bugs that occur only in certain situations, making them hard to reproduce reliably and fix.
16.1.4 Ways to Implement Threads
Creating threads by calling the operating system’s API is called the 1:1 model, meaning one operating-system thread corresponds to one language thread. Its advantage is that it requires a smaller runtime.
A language can implement threads itself, also called green threads. This is the M:N model, meaning M green threads correspond to N system threads. It requires a larger runtime.
Each model has its own strengths and weaknesses, so Rust must balance them against runtime support.
Aside from assembly language, every programming language has some runtime.
Even C/C++, which have very little runtime functionality, still have a small runtime. That is why they can produce small binaries and remain usable in many situations together with other languages.
Some languages add more runtime features to provide more capabilities, such as Java, C#, and Go.
For Rust, the goal is to keep the runtime as close to nonexistent as possible so that it is easy to interoperate with C and achieve high performance. Therefore, the Rust standard library only provides 1:1-thread support.
However, because Rust has strong low-level abstraction capabilities, the community also provides many third-party crates that support the M:N model.
16.1.5 Creating Threads With spawn
You can create a new thread with thread::spawn. It takes one argument: a closure containing the code to run in the new thread.
Take a look at this example:
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
This closure takes no arguments. Its logic is simple: loop from 1 to 10 (not including 10), print each number, and sleep for 1 millisecond in every iteration.
The main thread also has a loop from 1 to 5 (not including 5), printing each number and sleeping for 1 millisecond in every iteration.
Because the spawned thread loops from 1 to 10 while the main thread loops from 1 to 5, the main thread finishes first. Rust then ends the program as soon as the main thread finishes, regardless of whether other threads are still running. The output from the main thread and the spawned thread should appear interleaved.
Output (thread interleaving is nondeterministic; this is a representative local run):
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
In this sample, after the main thread prints 4 it is about to finish; the spawned thread still gets a little more time and prints two more lines (hi number 4 and hi number 5) before the program shuts down.
This code does not guarantee that the other thread will finish execution, so we need JoinHandle.
16.1.6 Waiting for All Threads to Finish With JoinHandle
The return type of thread::spawn is JoinHandle. This type owns the handle to the spawned thread, and you can wait for that thread to finish by calling its join method.
Calling handle.join() blocks the currently running thread until the thread represented by handle finishes.
Take a look at this example:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
- Assign the return value of
thread::spawntohandle. - Finally, call
joinonhandleand thenunwrap. This blocks the current thread—in this example, the main thread—until the thread represented byhandlefinishes. The reason forunwrapis thathandle.join()returns aResult. If the thread completes successfully, it returnsOk(T), whereTis the thread’s return value. If the thread panics while running, it returnsErr(e), whereeis the error information. If you are sure the thread will not panic, you can callunwrapdirectly to simplify the code and ignore theErrbranch.
Output (thread interleaving is nondeterministic; this is a representative local run):
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
After the main thread prints "hi number 4 from the main thread!", it waits on join, so the spawned thread can keep printing through 9 instead of being cut off when the main loop ends.
Now let’s see what happens when handle.join() is moved before the for loop in main, as shown below:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
Output:
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
This time, the spawned thread finishes first, and only then does the main thread run its loop.
Using move Closures
move closures are often used together with thread::spawn, because they let you use data from another thread. In other words, when a thread is created, ownership of a value is moved from one thread to another.
Take a look at this example:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();}
- A
Vectornamedvis created inmain. - The new thread uses
vand prints it. - Finally,
handle.join().unwrap()makes the main thread wait for the spawned thread to finish.
Output:
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:5:32
|
5 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
6 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:5:18
|
5 | let handle = thread::spawn(|| {
| __________________^
6 | | println!("Here's a vector: {v:?}");
7 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
5 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
The error says the closure borrows v because the compiler infers that the closure only needs a borrow, but the closure’s lifetime may outlive v.
For example:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
drop(v);
handle.join().unwrap();
}
While the closure is running in the spawned thread, the main thread may already have reached drop(v) and destroyed v, so v can no longer be used in the spawned thread.
The simplest fix is to move ownership of v into the closure. Just write move before the || pipe:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
The downside is that the main thread can no longer use v.
Top comments (0)