Using move Closures with Threads
We'll often use the move
keyword with closures passed to thread::spawn
because the closure will then take ownership of the values it uses from the environment, thus transferring ownership of those values from one thread to another. In "Capturing the Environment with Closures", we discussed move
in the context of closures. Now we'll concentrate more on the interaction between move
and thread::spawn
.
Notice in Listing 16-1 that the closure we pass to thread::spawn
takes no arguments: we're not using any data from the main thread in the spawned thread's code. To use data from the main thread in the spawned thread, the spawned thread's closure must capture the values it needs. Listing 16-3 shows an attempt to create a vector in the main thread and use it in the spawned thread. However, this won't work yet, as you'll see in a moment.
Filename: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
Listing 16-3: Attempting to use a vector created by the main thread in another thread
The closure uses v
, so it will capture v
and make it part of the closure's environment. Because thread::spawn
runs this closure in a new thread, we should be able to access v
inside that new thread. But when we compile this example, we get the following error:
error[E0373]: closure may outlive the current function, but it borrows `v`,
which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {:?}", v);
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
Rust infers how to capture v
, and because println!
only needs a reference to v
, the closure tries to borrow v
. However, there's a problem: Rust can't tell how long the spawned thread will run, so it doesn't know whether the reference to v
will always be valid.
Listing 16-4 provides a scenario that's more likely to have a reference to v
that won't be valid.
Filename: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
drop(v); // oh no!
handle.join().unwrap();
}
Listing 16-4: A thread with a closure that attempts to capture a reference to v
from a main thread that drops v
If Rust allowed us to run this code, there's a possibility that the spawned thread would be immediately put in the background without running at all. The spawned thread has a reference to v
inside, but the main thread immediately drops v
, using the drop
function we discussed in Chapter 15. Then, when the spawned thread starts to execute, v
is no longer valid, so a reference to it is also invalid. Oh no!
To fix the compiler error in Listing 16-3, we can use the error message's advice:
help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
By adding the move
keyword before the closure, we force the closure to take ownership of the values it's using rather than allowing Rust to infer that it should borrow the values. The modification to Listing 16-3 shown in Listing 16-5 will compile and run as we intend.
Filename: src/main.rs
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();
}
Listing 16-5: Using the move
keyword to force a closure to take ownership of the values it uses
We might be tempted to try the same thing to fix the code in Listing 16-4 where the main thread called drop
by using a move
closure. However, this fix will not work because what Listing 16-4 is trying to do is disallowed for a different reason. If we added move
to the closure, we would move v
into the closure's environment, and we could no longer call drop
on it in the main thread. We would get this compiler error instead:
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not
implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {:?}", v);
| - variable moved due to use in
closure
...
10 | drop(v); // oh no!
| ^ value used here after move
Rust's ownership rules have saved us again! We got an error from the code in Listing 16-3 because Rust was being conservative and only borrowing v
for the thread, which meant the main thread could theoretically invalidate the spawned thread's reference. By telling Rust to move ownership of v
to the spawned thread, we're guaranteeing Rust that the main thread won't use v
anymore. If we change Listing 16-4 in the same way, we're then violating the ownership rules when we try to use v
in the main thread. The move
keyword overrides Rust's conservative default of borrowing; it doesn't let us violate the ownership rules.
Now that we've covered what threads are and the methods supplied by the thread API, let's look at some situations in which we can use threads.