为什么在涉及线程时必须使用“move”关键字;为什么我永远不想要这种行为?

dar*_*nge 7 concurrency multithreading move-semantics rust

例如(取自Rust 文档):

let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("Here's a vector: {:?}", v);
});
Run Code Online (Sandbox Code Playgroud)

这不是关于做什么 的问题move,而是关于为什么需要指定.

如果您希望闭包拥有外部 value 的所有权,是否有理由不使用move关键字?如果move经常在这些情况下需要,有没有为什么存在任何理由move不能只是暗示/省略?例如:

let v = vec![1, 2, 3];
let handle = thread::spawn(/* move is implied here */ || {
    // Compiler recognizes that `v` exists outside of this closure's
    // scope and does black magic to make sure the closure takes
    // ownership of `v`.
    println!("Here's a vector: {:?}", v);
});
Run Code Online (Sandbox Code Playgroud)

上面的例子给出了以下编译错误:

closure may outlive the current function, but it borrows `v`, which is owned by the current function
Run Code Online (Sandbox Code Playgroud)

当错误通过添加 神奇地消失时move,我不禁自问:为什么我想要这种行为?


我并不是在暗示所需的语法有什么问题。我只是想move从比我更了解 Rust 的人那里获得更深入的了解。:)

Lam*_*iry 7

这完全是关于生命周期注释,以及 Rust 很久以前做出的设计决策。

看,您的thread::spawn示例无法编译的原因是因为它需要一个'static闭包。由于新线程可以比产生它的代码运行更长的时间,我们必须确保在调用者返回后任何捕获的数据都保持活动状态。正如您所指出的,解决方案是将数据的所有权传递给move.

但是'static约束是一个生命周期注解,而 Rust 的一个基本原则是生命周期注解永远不会影响运行时行为。换句话说,生命周期注解只是为了让编译器相信代码是正确的;他们不能改变代码的作用

如果 Rust 是move根据被调用者是否期望来推断关键字的'static,那么thread::spawn当捕获的数据被丢弃时,改变生命周期可能会改变。这意味着生命周期注解正在影响运行时行为,这违背了这一基本原则。我们不能打破这个规则,所以move关键字保持不变。


附录:为什么生命周期注释会被擦除?

  • 让我们可以自由地改变生命周期推断的工作方式,这允许像非词法生命周期(NLL)这样的改进。

  • 因此,像mrustc这样的替代 Rust 实现可以通过忽略生命周期来节省工作量。

  • 大部分编译器都假设生命周期以这种方式工作,因此要实现这一点,将需要付出巨大的努力并获得可疑的收益。(请参阅Aaron Turon 的这篇文章;它是关于专业化,而不是闭包,但它的要点也同样适用。)


Evi*_*Tak 5

这里实际上有一些事情在起作用。为了帮助回答您的问题,我们必须首先了解为什么move存在。

Rust 有 3 种类型的闭包:

  1. FnOnce,一个消耗其捕获变量的闭包(因此只能调用一次),
  2. FnMut,一个可变地借用其捕获变量的闭包,以及
  3. Fn,一个不变地借用其捕获变量的闭包。

当你创建一个闭包时,Rust 会根据闭包如何使用环境中的值来推断要使用哪个特征。闭包捕获其环境的方式取决于其类型。AFnOnce按值捕获(如果类型可以,可以是移动或复制Copy)、FnMut可变借用和Fn不可变借用。但是,如果您move在声明闭包时使用关键字,它将始终“按值捕获”,或者在捕获它之前获得环境的所有权。因此,move关键字与FnOnces无关,但它改变了Fns 和FnMuts 捕获数据的方式。

就您的示例而言,Rust 将闭包的类型推断为 a Fn,因为println!只需要引用它正在打印的值(您链接的 Rust 书页在解释没有 的错误时谈到了这一点move)。因此,闭包尝试借用v,并且适用标准的生命周期规则。由于thread::spawn要求传递给它的闭包具有'static生命周期,因此捕获的环境也必须具有'static生命周期,该生命周期v不会超过生命周期,从而导致错误。因此,您必须明确指定您希望闭包拥有v.

这可以通过将闭包更改为编译器会推断为FnOnce-- 的东西来进一步举例说明|| v,作为一个简单的例子。由于编译器推断闭包是 a FnOnce,因此v默认情况下它按值捕获,并且该行let handle = thread::spawn(|| v);编译时不需要move.

  • 我不确定这是否正确。“move”关键字和“Fn”特征是正交的。而“Vec<T>”*永远不会*“复制”。 (4认同)
  • @EvilTak 一个好的经验法则是要记住,与可能包含任意代码的 C++ 复制构造函数不同,Rust 将复制实现为值本身占用的内存的直接按位复制(或者表现得好像是这样实现的) 。由于“Vec”由三个机器字组成,其中一个是指向堆分配数据的指针,因此进行“Vrc”复制将导致两个“Vec”指向相同的数据,从而在“Drop”时间造成严重破坏并导致在此之前的别名问题。 (2认同)