简体   繁体   English

clone() 和 move 的惊人行为

[英]Surprising behavior of clone() and move

I'm attempting to optimize an application through smart cloning and borrowing, and I'm observing the following behavior.我正在尝试通过智能克隆和借用来优化应用程序,并且我正在观察以下行为。 The program below wouldn't work:下面的程序不起作用:

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

fn main() {
    let my_string: String = "ABCDE".to_string();
    f( my_string );
    f( my_string );
}

It generates the well-known "used after move" error.它会产生众所周知的“移动后使用”错误。

7 |     f( my_string );
  |        --------- value moved here
8 |     f( my_string );
  |        ^^^^^^^^^ value used here after move

This can be solved by cloning my_string .这可以通过克隆my_string来解决。 The program below works fine:下面的程序工作正常:

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

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

However, if you use the same approach in a multi-threaded environment, cloning doesn't help any longer.但是,如果您在多线程环境中使用相同的方法,克隆就不再有用了。 When the function calls are embedded in threads:当函数调用嵌入到线程中时:

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() ); } );
}

the program generates the "used after move" error again:程序再次生成“移动后使用”错误:

10 |     thread::spawn( move || { f( my_string.clone() ); } );
   |                    ^^^^^^^      --------- use occurs due to use in closure
   |                    |
   |                    value used here after move

However, you can remedy this by moving the thread into the function, with the same net effect:但是,您可以通过将线程移动到函数中来解决这个问题,具有相同的净效果:

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() );
}

The above program works fine.上面的程序工作正常。 Or, if you prefer, you can clone my_string in advance and use the clone in the second function call:或者,如果您愿意,可以提前克隆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 ); } );
}

It looks a little like trial and error, but some theory can perhaps explain it.它看起来有点像试错法,但一些理论或许可以解释它。

EDIT : There is another question regarding the "used after move" error.编辑:还有一个关于“移动后使用”错误的问题。 The other question discusses the effect of to_string() , while this one discusses clone() in a threaded environment.另一个问题讨论了to_string()的影响,而这个问题讨论了线程环境中的clone()

EDIT2 : After reading the answers, the conclusion is as follows: In the function call f(my_string.clone()) cloning is done first and moving the cloned variable into the function later, so my_string is never moved. EDIT2 :阅读答案后,结论如下:在函数调用f(my_string.clone())中,首先进行克隆,然后将克隆的变量移动到函数中,因此永远不会移动my_string In the closure move || { f(my_string.clone())在闭包中move || { f(my_string.clone()) move || { f(my_string.clone()) moving of my_string into the closure is done first , so cloning my_string cannot solve it. move || { f(my_string.clone())首先将my_string移动到闭包,因此克隆my_string无法解决它。 It is a difference between a function and a closure.这是函数和闭包之间的区别。 Thread has nothing to do with this except that it imposes move in the closure. Thread与此无关,只是它在闭包中强行move

The move keyword means that Rust now has to move all used variables from outside into the closure. move关键字意味着 Rust 现在必须将所有使用的变量从外部移动到闭包中。 So by using my_string inside of move || {}所以通过在move || {}中使用my_string move || {} you moved it in. If you just want to use a reference inside of the closure you can borrow outside the closure: move || {}您将其移入。如果您只想在闭包内使用引用,您可以在闭包外借用:

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

fn main() {
    let my_string: String = "ABCDE".to_string();
    {
        // Take the reference outside of the `move`
        // closures so only the reference but not the
        // actual string gets moved inside.
        //
        // Since shared references are `Copy` they get
        // copied instead and you can reuse them.
        let my_string: &String = &my_string;
        let a = move || {
            f(my_string.clone());
        };
        let b = move || {
            f(my_string.clone());
        };
    }
}

But because you want to use it in a different thread wihch might outlive the current function the reference would have to be 'static .但是因为你想在不同的线程中使用它,它可能比当前函数的寿命更长,所以引用必须是'static

So to avoid moving my_string into the closure which gets passed to another thread you have to do the clone outside of the closure.因此,为了避免将my_string移动到传递给另一个线程的闭包中,您必须在闭包之外进行clone

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

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

However, if you use the same approach in a multi-threaded environment, cloning doesn't help any longer.但是,如果您在多线程环境中使用相同的方法,克隆就不再有用了。 [...] It looks a little like trial and error, but some theory can perhaps explain it. [...] 它看起来有点像反复试验,但一些理论也许可以解释它。

It does help if you do it right.如果你做对了,它确实有帮助。 The problem here is that a move closure means the value gets moved into the closure before the closure runs 1 .这里的问题是move闭包意味着值在闭包运行之前被移动到闭包中1

So when you write所以当你写

    thread::spawn(move || { f( my_string.clone()); });

what happens is发生的事情是

  1. create the closure, move my_string inside the closure创建闭包,将my_string移动到闭包内
  2. spawn the thread产生线程
  3. clone my_string克隆my_string
  4. call f with the clone用克隆调用 f

By the time you're cloning my_string it's already way too late, because it's been moved from the outer function to the inside the closure and thread.当你克隆my_string时已经太晚了,因为它已经从外部函数移到了闭包和线程的内部。 It's as if you'd try to fix the original snippet by changing the contents of f thus:就好像您试图通过更改f的内容来修复原始片段:

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

fn main() {
    let my_string = "ABCDE".to_string();
    f(my_string);
    f(my_string);
}

That obviously doesn't fix anything.这显然不能解决任何问题。

The usual solution for this is the so-called "precise capture pattern".通常的解决方案是所谓的“精确捕获模式”。 "Capture clause" comes from C++ where it's a native feature, but in Rust it's not. “Capture 子句”来自 C++,它是本机功能,但在 Rust 中不是。 What you do is that instead of creating a closure directly, you create and return the closure from a block, and before creating and returning the closure you can create a bunch of bindings which then get moved into the closure.你所做的是,不是直接创建一个闭包,而是从一个块中创建并返回闭包,在创建和返回闭包之前,你可以创建一堆绑定,然后将它们移动到闭包中。 It essentially provides a "closure setup" which is isolated:它本质上提供了一个隔离的“闭包设置”:

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); }
});

Incidentally an other option if you don't need to modify the String is to just put it in an Arc .顺便说一句,如果您不需要修改String ,另一个选择是将它放在Arc中。 Though you still need to clone outside the closure and move the arc inside the closure.尽管您仍然需要在闭包外克隆并将弧线移动到闭包内。 The advantage is that cloning an arc just increments its refcount, it's atomic so it's not free, but it can be cheaper than cloning a complex / expensive object.优点是克隆一个弧只会增加它的引用计数,它是原子的,所以它不是免费的,但它比克隆一个复杂/昂贵的对象便宜。


[1]: technically that can also happen with non-move closures, more precisely move closure will move (/ copy ) everything it refers to, while a non-move closure may move or just borrow depending how the item is being used. [1]:从技术上讲,非移动闭包也会发生这种情况,更准确地说, move闭包将move (/ copy )它所指的所有内容,而非移动闭包可能会移动或只是借用,具体取决于项目的使用方式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM