Trying to move/copy value into non-move async block

I'm trying to join a variable number of futures concurrently, using futures::future::join_all. Creating the futures and joining them happens in a &mut self method. Each concurrent task, however, needs to execute a method on &self.

Working non-asynchronous (and non-concurrently), this works fine:

struct S {
    sum: i32,
    multiplier: i32,
}

impl S {
    fn new(multiplier: i32) -> Self {
        S { multiplier, sum: 0 }
    }
    fn do_something(&self, payload: i32) -> i32 {
        println!("Processed: {}", payload);
        self.multiplier * payload
    }
    fn run(&mut self) {
        let tasks = (0..3).map(|i| self.do_something(i));
        let results = tasks.collect::<Vec<_>>();
        for result in results {
            self.sum += result;
        }
    }
}

fn main() {
    let mut s = S::new(10);
    s.run();
    println!("Sum: {}", s.sum);
}

But when I (naïvely) go to async, it wont' work:
(I'll provide a solution later in this thread)

impl S {
    fn new(multiplier: i32) -> Self {
        S { multiplier, sum: 0 }
    }
    async fn do_something(&self, payload: i32) -> i32 {
        println!("Processed: {}", payload);
        self.multiplier * payload
    }
    async fn run(&mut self) {
        let tasks = (0..3).map(|i| async { self.do_something(i).await });
        let results = futures::future::join_all(tasks).await;
        for result in results {
            self.sum += result;
        }
    }
}

#[tokio::main]
async fn main() {
    let mut s = S::new(10);
    s.async_run().await;
    println!("Sum: {}", s.sum);
}

For some reason, i gets borrowed (instead of copied). Is that correct behavior of the compiler? I get the following error message:

error[E0373]: async block may outlive the current function, but it borrows `i`, which is owned by the current function
  --> src/main.rs:15:42
   |
15 |         let tasks = (0..3).map(|i| async { self.do_something(i).await });
   |                                          ^^^^^^^^^^^^^^^^^^^^-^^^^^^^^^
   |                                          |                   |
   |                                          |                   `i` is borrowed here
   |                                          may outlive borrowed value `i`
   |
note: async block is returned here
  --> src/main.rs:15:36
   |
15 |         let tasks = (0..3).map(|i| async { self.do_something(i).await });
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the async block to take ownership of `i` (and any other referenced variables), use the `move` keyword
   |
15 |         let tasks = (0..3).map(|i| async move { self.do_something(i).await });
   |                                          ++++

I'm trying to force the move. I learned I can do that by writing let x = x; so I write:

        let tasks = (0..3).map(|i| {
            let i: i32 = i; // trying to copy here
            async {
                let i: i32 = i; // trying to copy here as well
                self.do_something(i).await
            }
        });

But that doesn't work either:

error[E0373]: async block may outlive the current function, but it borrows `i`, which is owned by the current function
  --> src/main.rs:17:19
   |
17 |               async {
   |  ___________________^
18 | |                 let i: i32 = i; // trying to copy here as well
   | |                              - `i` is borrowed here
19 | |                 self.do_something(i).await
20 | |             }
   | |_____________^ may outlive borrowed value `i`
   |
note: async block is returned here
  --> src/main.rs:17:13
   |
17 | /             async {
18 | |                 let i: i32 = i; // trying to copy here as well
19 | |                 self.do_something(i).await
20 | |             }
   | |_____________^
help: to force the async block to take ownership of `i` (and any other referenced variables), use the `move` keyword
   |
17 |             async move {
   |                   ++++

Yeah, I know, I should add "move", but I'm not giving up yet!

First let's see what happens if I write *i. I get:

error[E0614]: type `i32` cannot be dereferenced
  --> src/main.rs:18:30
   |
18 |                 let i: i32 = *i; // trying to copy here as well
   |                              ^^

So what!? i isn't a reference, but an i32. Why can't I copy it?

Perhaps the reason is that the copy doesn't get executed until the async block is polled/awaited? Is that documented somewhere? I doubt it is a flaw in the compiler, so I wonder what's going on here.

One solution is to use async move. It doesn't turn out trival though:

        let tasks = (0..3).map(|i| async move { self.do_something(i).await });

This gives:

error[E0507]: cannot move out of `self`, a captured variable in an `FnMut` closure
…
error[E0382]: use of moved value: `self`
…

Instead I have to do:

    async fn run(&mut self) {
        let tasks = (0..3).map(|i| {
            let this: &_ = self;
            async move { this.do_something(i).await}
        });
        let results = futures::future::join_all(tasks).await;
        for result in results {
            self.sum += result;
        }
    }

That works, and I get:

Processed: 0
Processed: 1
Processed: 2
Sum: 30

(Playground with the full example if you want to experiment on it.)

My questions:

  1. Is it correct that let x = x; can force a move for (non-async) closures, but doesn't force a move for an async (non-move) block? If yes, why? And is this documented somewhere?
  2. Is there any way to fix the problem (see playground link above) without using async move? This is more a theoretical question. I'm fine to use move, but I would like to understand if there are any alternatives. I always thought I can variable-wise override the behaviour with let x = x; inside the block (to force move/copy) or let x = &x; (to force borrow).

If the type of x is Copy, then let x = x will never force a move, AFAIK, neither for closures nor for async blocks. Note that the difference between your synchronous and asynchronous example is also that the latter returns an async block from a closure.

The particular problem here will probably be fixed if we get async closures in the future, i.e. something like async |i| { ... } would then (hopefully / probably) be equivalent to |i| async { ... /* but *actually* move i no matter what type */ }.

Regarding your solution, I would maybe find

let this = &*self;
let tasks = (0..3).map(|i| async move { this.do_something(i).await });

even a bit nicer (mainly because it's fewer lines).

I like that syntax too! :+1:

Actually let this = &self; would work too, but I guess that's only because of method invocation (and would have the weird type &&mut Self, I think, which could make problems in other cases).

Testing further, I figured out that the problem I have isn't related to async blocks vs closures, but it is indeed the returning an async block (or closure, see example below) from a closure (or block).

I boiled it down to an example not involving async at all. Consider the following:

fn main() {
    let a = 1;
    // Move can be omitted here:
    let closure1 = move || {
        println!("a = {}", a);
    };
    drop(a);
    closure1();

    let closure2 = {
        let b = 2;
        // Move is mandatory here:
        move || {
            println!("b = {}", b);
        }
    };
    closure2();
}

Shouldn't these two cases be the same? (Playground)

What am I missing here?

a is still in scope when you use closure1; you don't try to move ownership of closure1 to some place where a is out of scope. The drop just drops a copy, because i32: Copy. Try replacing 1 with String::new() (with or without move).

Compare:

fn main() {
    let a = 1;
    let borrow = &a;
    drop(a);
    println!("Using borrow of a: {}", borrow);

    let borrow = {
        let b = 2;
        &b
    };
    println!("Using borrow of b: {}", borrow);
}

You may also want to write functions that borrow or take by value to use inside your closures when writing experiments or tests like this. Macros occlude what they do with their arguments.

2 Likes

Thanks, now everything makes sense.

That's what got me confused during all my testing.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.