Why doesn't Rust async block drop a value at the end of its scope?

Viewed 198

I ran into a curious issue while debugging some async code in Rust. Mainly, I have the following async block which is part of a test:

let s = Signal::new(0);

let s_clone = s.clone();

assert_eq!(s_clone.clone().await, 0);

assert_eq!(s.clone().await, 0);

join![s.for_each(|_| async {}), async {
  s_clone.set_and_wait(7).await;
  println!("s_clone should drop here...");
  drop(s_clone);
}];

The join![] code in this part doesn't matter too much, what matters is that the Signal I implemented is shared in such a way that only where there are no more unused signals do they stop and drop their values. Thus, if I omit the explicit drop(s_clone);, s_clone won't get dropped, causing join![] to never be able to complete.

The Question

In synchronous code, when a value reaches the end of the block, it just get's gropped, but why not in async? Why do I have to manually drop the value?

Additional Context

In case anyone is curious, I've implemented both Future and Stream for Signal. The future will only complete when all other pending signals cloned from the same source signal have finished reacting to any of their changes.

1 Answers

drop(s_clone) makes the async block consume s_clone and thus forces s_clone to be moved into it. (This is not specific to drop() - doing anything that consumes the value would have the same effect.) Omitting the drop() reverts to the default closure behavior of borrowing the value. When the value is borrowed, it remains owned by the surrounding environment and so it won't (and can't) be dropped by the async block, which only holds a reference.

To force the async block to drop s_clone without an explicit drop(), use the move keyword, i.e. change async to async move. This will ensure that ownership of s_clone (and any other captured value) is moved into the async block, which will then drop it prior to exit.

In synchronous code, when a value reaches the end of the block, it just get's gropped, but why not in async? Why do I have to manually drop the value?

The exact same thing can happen in synchronous code. Here is a non-async example equivalent to yours, where you can either uncomment the move or the drop() to have the value owned and dropped by the closure:

struct Dropper;

impl Drop for Dropper {
    fn drop(&mut self) {
        println!("dropper dropped");
    }
}

fn main() {
    let dropper = Dropper;
    {
        let closure = /*move*/ || {
            let _ = &dropper;  // capture dropper
            //drop(dropper);
        };
        closure();
    }
    println!("closure dropped")
}

Playground

Related