Future holds a reference even after it was dropped

I thought I've understood how async works in Rust but I was very very wrong...

So here is a simplified example:

async fn process<F1: Future, F2: Future>(
    f1: &mut F1,
    f2: &mut F2,
) -> (Option<F1::Output>, Option<F2::Output>) {
    todo!()
}

async fn test() {
    let mut a = MyObject;
    let mut b = MyObject;

    let mut f1 = a.do_something();
    let mut f2 = b.do_something();
    
    loop {
        // commenting this out doesn't help
        let (f1_result, f2_result) = process(&mut f1, &mut f2).await;

        if f1_result.is_some() {
            drop(f1); // does nothing
            f1 = a.do_something(); // still thinks f1 is not dropped
        }
        
        if f2_result.is_some() {
            drop(f2); // does nothing
            f2 = b.do_something(); // still thinks f1 is not dropped
        }
    }
}

General description:

  • objects a and b produce futures that I want to observe
  • process method waits for any future to complete and returns their results when completed
  • if any future completes - it is restarted / recreated

But compiler doesn't agree with me (same goes for f2):

error[E0499]: cannot borrow `a` as mutable more than once at a time
  --> src/main.rs:31:18
   |
22 |     let mut f1 = a.do_something();
   |                  ---------------- first mutable borrow occurs here
...
31 |             f1 = a.do_something(); // still thinks f1 is not dropped
   |             --   ^^^^^^^^^^^^^^^^ second mutable borrow occurs here
   |             |
   |             first borrow might be used here, when `f1` is dropped and runs the destructor for type `impl Future<Output = ()>`

Playground link:

So the questions are:

  1. why? why does it happens? error message doesn't really help. It says that object might be used during dropping the future, but I explicitly drop said future before creating a new one...
  2. how can I fix it? preferrably without futures or other crates except tokio.

Thanks.

There's a more general principle here, independent of async: the borrow checker doesn't know what drop means, and doesn't conclude that drop(f1) is anything different than, say, spawn(f1). All it knows is that f1 has a borrow of a, which is assumed not to end until either the variable f1 is not being used any more (not true here because it's used in the next iteration of the loop) or the borrow created implicitly by a.do_something() is out of scope (not true because a is outside of the loop).

The borrow checker cares about scopes, and about lifetimes in function signatures, not what a function concretely does with your variables, even if that function is drop().

1 Like

following your description, why then this doesn't help?

            {
                        let _ = f1;
            }
            f1 = a.do_something(); // still thinks f1 is not dropped

Shouldn't borrow checker know that f1 is no longer used before I call do_something? I no longer pass ownership anywhere.
Because it still says that f1 will be dropped during assignment.

It seams to be related to futures after all:

struct Abc;
struct Holder<'a>(&'a mut Abc);

impl Abc {
    fn borrow(&mut self) -> Holder {
        Holder(self)
    }
    async fn borrow_async(&mut self) {
    }
}

fn test1() {
    let mut a = Abc;

    let mut borrower = a.borrow();
    loop {
        borrower = a.borrow();
    }
}

fn test2() {
    let mut a = Abc;

    let mut borrower = a.borrow_async();
    loop {
        borrower = a.borrow_async();
    }
}

test1 works, while test2 fails with the same error.

This is #70919 I think. Certainly it's getting it wrong that f1 drops twice.

A version with no async.

2 Likes

If you make Holder<'_> implement Drop, test1 also fails.

impl Drop for Holder<'_> { fn drop(&mut self) {} }

Thanks. So I was tecnically correct with my code.
BTW, do you know if there are some workarounds for this or the only solution is to restructure my code?

You can manually drop.

Edit: Macros can ease some of the pain.

2 Likes

it is not safe for unwinding and error-prone for early returns but at least something. thanks.

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.