Why is the "move" keyword necessary when it comes to threads; why would I ever not want that behavior?

It's all about lifetime annotations, and a design decision Rust made long ago.

See, the reason why your thread::spawn example fails to compile is because it expects a 'static closure. Since the new thread can run longer than the code that spawned it, we have to make sure that any captured data stays alive after the caller returns. The solution, as you pointed out, is to pass ownership of the data with move.

But the 'static constraint is a lifetime annotation, and a fundamental principle of Rust is that lifetime annotations never affect run-time behavior. In other words, lifetime annotations are only there to convince the compiler that the code is correct; they can't change what the code does.

If Rust inferred the move keyword based on whether the callee expects 'static, then changing the lifetimes in thread::spawn may change when the captured data is dropped. This means that a lifetime annotation is affecting runtime behavior, which is against this fundamental principle. We can't break this rule, so the move keyword stays.


Addendum: Why are lifetime annotations erased?

  • To give us the freedom to change how lifetime inference works, which allows for improvements like non-lexical lifetimes (NLL).

  • So that alternative Rust implementations like mrustc can save effort by ignoring lifetimes.

  • Much of the compiler assumes that lifetimes work this way, so to make it otherwise would take a huge effort with dubious gain. (See this article by Aaron Turon; it's about specialization, not closures, but its points apply just as well.)


There are actually a few things in play here. To help answer your question, we must first understand why move exists.

Rust has 3 types of closures:

  1. FnOnce, a closure that consumes its captured variables (and hence can only be called once),
  2. FnMut, a closure that mutably borrows its captured variables, and
  3. Fn, a closure that immutably borrows its captured variables.

When you create a closure, Rust infers which trait to use based on how the closure uses the values from the environment. The manner in which a closure captures its environment depends on its type. A FnOnce captures by value (which may be a move or a copy if the type is Copyable), a FnMut mutably borrows, and a Fn immutably borrows. However, if you use the move keyword when declaring a closure, it will always "capture by value", or take ownership of the environment before capturing it. Thus, the move keyword is irrelevant for FnOnces, but it changes how Fns and FnMuts capture data.

Coming to your example, Rust infers the type of the closure to be a Fn, because println! only requires a reference to the value(s) it is printing (the Rust book page you linked talks about this when explaining the error without move). The closure thus attempts to borrow v, and the standard lifetime rules apply. Since thread::spawn requires that the closure passed to it have a 'static lifetime, the captured environment must also have a 'static lifetime, which v does not outlive, causing the error. You must thus explicitly specify that you want the closure to take ownership of v.

This can be further exemplified by changing the closure to something that the compiler would infer to be a FnOnce -- || v, as a simple example. Since the compiler infers that the closure is a FnOnce, it captures v by value by default, and the line let handle = thread::spawn(|| v); compiles without requiring the move.


The existing answers have great information, which led me to an understanding that is easier for me to think about, and hopefully easier for other Rust newcomers to get.


Consider this simple Rust program:

fn print_vec (v: &Vec<u32>) {
    println!("Here's a vector: {:?}", v);
}

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    print_vec(&v); // `print_vec()` borrows `v`
    v.push(4);
}

Now, asking why the move keyword can't be implied is like asking why the "&" in print_vec(&v) can't also be implied.

Rust’s central feature is ownership. You can't just tell the compiler, "Hey, here's a bunch of code I wrote, now please discern perfectly everywhere I intend to reference, borrow, copy, move, etc. Kthnxsbye!" Symbols and keywords like & and move are a necessary and integral part of the language.

In hindsight, this seems really obvious, and makes my question seem a little silly!