简体   繁体   中英

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 . 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:

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.

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. In the closure 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. 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.

The move keyword means that Rust now has to move all used variables from outside into the closure. So by using my_string inside of move || {} move || {} you moved it in. If you just want to use a reference inside of the closure you can borrow outside the closure:

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 .

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.

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 .

So when you write

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

what happens is

  1. create the closure, move my_string inside the closure
  2. spawn the thread
  3. clone my_string
  4. call f with the clone

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. It's as if you'd try to fix the original snippet by changing the contents of f thus:

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. 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 . 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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