I don’t see how you could guarantee (in a typesafe sense) that a future will be run to completion since the executor not only must not drop it, but also must continue polling it for it to make progress
I haven't thought about it, but I honestly think it doesn't matter too much. Executors are rare. There's maybe a handfull of them required. It's ok for them to contain unsafe code that holds up certain guarantees. They already do that today, by implementing wake()
in an unsafe way. Being hold up to the contract to poll each task to completion is just one more addition to that.
And while its not guaranteed that a hard cancelled future won’t be leaked all well behaved combinators and executors will drop futures they stop executing, allowing some amount of clean up on cancellation in correctly implemented code.
It's not about combinators behaving correctly. It's about reasoning in async code, and understanding transaction levels. E.g. I wanted to write some kind of rate limiting in async code during the last days. I really wanted to write that in the simple fashion that is equivalent to:
await!(self.async_semaphore.aquire(amount));
{
/** Do something very long that might yield multiple times */
}
await!(self.async_semaphore.release(amount));
That doesn't work, since the code might never arrive at release
. In a similar fashion an unsafe malloc
and a free
after a yield point would not be guaranteed to work.
In those cases it can be worked around by custom RAII guards. But it's important to know that those are required, and it doesn't necessarily work well for other code path where transactional semantics are required but there is not such a notion of guard that can be dropped unconditionally - e.g. if one has to execute situation-dependent cleanup code that also needs to be async
.