The idea that our story around cancellation is bad is confusing to me (but not the only idea of this thread of course). The fact that all futures are trivially cancellable is a key advantage of our design. But of course people will write bugs if they depend on two actions on opposite sides of an await to be logically atomic instead of writing code that uses destructors to properly maintain state.
However, the problem with what we have so far is that destructors can only call synchronous code, but the “set up” is done in an async context, and so there are good reasons to want the “clean up” to be async as well. AsyncDrop
is on the list for the future of async/await. The rest of my post is some notes about this.
Probably it is entirely the responsibility of the executor library to guarantee that AsyncDrop
is called - and probably by spawning a new task on the destructor of its task wrapper. I don’t know how we could guarantee async drop to be called given that we don’t even provide a mechanism in the standard library to execute futures at all.
std would want to provide these two APIs:
trait AsyncDrop {
async fn drop(&mut self);
}
async fn drop_async<T>(item: T) { /* .... */ }
drop_async would be a magic function that destroys this type properly, including calling the drop glue as necessary on its fields and so on. AsyncDrop
would have the same magic limits as Drop
its impl must cover all valid instances of the type, you can’t call the method, and so on. (It’s not technically necessary to make Drop and AsyncDrop mutually exclusive to implement I believe, but maybe its a good idea.)
Then executors would use their own spawn API to spawn(async_drop(task))
when a task gets dropped (whether because it resolved or because it has been cancelled).