为什么 .clone() 在闭包中使用时不会阻止移动?

Kal*_*son 1 closures ownership rust

我正在尝试通过智能克隆和借用来优化应用程序,并且我正在观察以下行为。下面的程序将无法运行:

fn f( string: String) {
    println!("{}", string );
}

fn main() {
    let my_string: String = "ABCDE".to_string();
    f( my_string );
    f( my_string );
}
Run Code Online (Sandbox Code Playgroud)

它会生成众所周知的“移动后使用”错误。

fn f( string: String) {
    println!("{}", string );
}

fn main() {
    let my_string: String = "ABCDE".to_string();
    f( my_string );
    f( my_string );
}
Run Code Online (Sandbox Code Playgroud)

这个可以通过克隆来解决my_string。下面的程序运行良好:

fn f( string: String) {
    println!("{}", string );
}

fn main() {
    let my_string: String = "ABCDE".to_string();
    f( my_string.clone() );
    f( my_string.clone() );
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您在多线程环境中使用相同的方法,克隆就不再有帮助了。当函数调用嵌入到线程中时:

use std::thread;

fn f( string: String) {
    println!("{}", string );
}

fn main() {
    let my_string: String = "ABCDE".to_string();
    thread::spawn( move || { f( my_string.clone() ); } );
    thread::spawn( move || { f( my_string.clone() ); } );
}
Run Code Online (Sandbox Code Playgroud)

程序再次生成“移动后使用”错误:

7 |     f( my_string );
  |        --------- value moved here
8 |     f( my_string );
  |        ^^^^^^^^^ value used here after move
Run Code Online (Sandbox Code Playgroud)

但是,您可以通过将线程移至函数中来解决此问题,具有相同的最终效果:

use std::thread;

fn f( string: String) {
    thread::spawn( move || { println!("{}", string ); } );
}

fn main() {
    let my_string: String = "ABCDE".to_string();
    f( my_string.clone() );
    f( my_string.clone() );
}
Run Code Online (Sandbox Code Playgroud)

上面的程序运行良好。或者,如果您愿意,可以提前克隆并在第二个函数调用my_string中使用克隆:

use std::thread;

fn f( string: String) {
    println!("{}", string );
}

fn main() {
    let my_string: String = "ABCDE".to_string();
    let my_second_string: String = my_string.clone();

    thread::spawn( move || { f( my_string.clone() ); } );
    thread::spawn( move || { f( my_second_string ); } );
}
Run Code Online (Sandbox Code Playgroud)

这看起来有点像反复试验,但某些理论也许可以解释它。

还有一个关于“移动后使用”错误的问题。另一个问题讨论 的效果,而这个问题则在线程环境中to_string()讨论。clone()

Mas*_*inn 6

但是,如果您在多线程环境中使用相同的方法,克隆就不再有帮助了。[...]这看起来有点像反复试验,但某些理论也许可以解释它。

如果你做得正确的话,它确实有帮助。这里的问题是闭包意味着在闭包运行1move之前该值被移入闭包。

所以当你写的时候

    thread::spawn(move || { f( my_string.clone()); });
Run Code Online (Sandbox Code Playgroud)

发生的事情是

  1. 创建闭包,my_string在闭包内移动
  2. 产生线程
  3. 克隆my_string
  4. 使用克隆调用 f

当您克隆时my_string已经太晚了,因为它已从外部函数移至闭包和线程内部。就好像您尝试通过更改以下内容来修复原始片段f

    thread::spawn(move || { f( my_string.clone()); });
Run Code Online (Sandbox Code Playgroud)

这显然不能解决任何问题。

通常的解决方案是所谓的“精确捕获模式”。“Capture 子句”来自 C++,它是一个原生特性,但在 Rust 中却不是。您所做的是,不是直接创建闭包,而是从块创建并返回闭包,在创建和返回闭包之前,您可以创建一堆绑定,然后将它们移动到闭包中。它本质上提供了一个隔离的“关闭设置”:

thread::spawn({
    // shadow the outer `my_string` with a clone of itself
    let my_string = my_string.clone();
    // then move the clone into the closure
    move || { f(my_string); }
});
Run Code Online (Sandbox Code Playgroud)

顺便说一句,如果您不需要修改,另一种选择String是将其放入Arc. 尽管您仍然需要在闭包外部克隆并将圆弧移动到闭包内部。优点是克隆弧只会增加其引用计数,它是原子的,因此不是免费的,但它比克隆复杂/昂贵的对象更便宜


[1]:从技术上讲,非移动闭包也可能发生这种情况,更准确地说,move闭包将move(/ copy) 它所引用的所有内容,而非移动闭包可能会移动或只是借用,具体取决于项目的使用方式。