Stabilizing Future APIs

The term "polling" tends to give the wrong impression of how things work- it made me think of something spinning in a loop, wasting work. So if that's your impression, suspend it while I try to describe the difference:

Push-based async is maybe better described as "callback-based." If you're familiar with pre-Promise Javascript or Node.js, that's the underlying pattern. To await some operation, you pass it a callback to invoke on completion. Async functions can be desugared to this pattern by automatically passing callback that advances their underlying state machine.

The downside here, especially in the context of Rust, is that the awaited operation now has to hold onto the state machine somehow. This means if you have a chain of async functions awaiting each other, each one gets allocated separately so the thing it's awaiting can hold onto it. That means you've got an actual graph of state machines all pointing at each other, so you probably need a garbage collector. It also means you can't really cancel anything without threading "cancellation tokens" everywhere, which only makes that graph more complicated.

Poll-based async is a lot closer to threads or coroutines. That chain of async functions awaiting each other is grouped into a single "task," which is "spawned" onto an executor and marked as ready to run. The executor polls tasks that are ready to run by handing them a "waker" associated with the task as a whole. Each async function passes the single waker, which it got from its caller, through to the thing it's awaiting. Only when the waker reaches an external event (like a network socket read) does it get signaled, marking the task as ready to run again.

This collapses the whole push-based pointer graph into a single task object- outer state machines contain inner state machines by value, and the whole thing is owned by the executor. External events still hold onto something, but it's just a waker instead of a callback in that graph. This also solves the cancellation problem, because you can just drop a whole task without worrying about someone trying to run a callback that depends on it.

(I left out one detail above- when polling a task that is resuming in some deeply nested async function, how does it get from the outer state machine to the one that actually needs to run? In Rust, each one re-polls the one it's awaiting to quickly rebuild the call stack. Then the innermost state machine either hands the waker to the next external event, or completes and returns to its awaiter, and this repeats all the way back out to the executor.)

/r/rust Thread Parent Link - github.com