4 ms·
Waiters and pollers are artifacts from the fact that Futures in Rust are lazily scheduled, unlike Javascript, for example, where Promises are eagerly scheduled
by joaonmatos 4y ago
Waiters and pollers are artifacts from the fact that Futures in Rust are lazily scheduled, unlike Javascript, for example, where Promises are eagerly scheduled and executed.
When you don't want to block your process and program in a reactive, non-blocking way, which is what futures and promises are an implementation of, what you are actually defining is something like this:
> "our process will do some synchronous work, then it will need to stop at point X to wait for asynchronous data"
> "after that data arrives, we can restart our process where it stopped, at point X, and do some more work until it hits point Y, where it will need to wait some more asynchronous data"
> "after that also completes, we can restart from Y, and process until the end of the function"
However, the problem that arises is _how do you store the variables that are being used in this function_? Particularly those values that would be saved in the stack, because by returning the function, the program needs to pop all those stack frames so that the caller can continue executing.
There are two ways of accomplishing this. The first way is to use stackful co-routines, aka threads. The OS does this, but platform threads have very high overhead, so many languages avoid it. You can also use the OS's reactive APIs or thread pools and implement your own scheduler with virtual threads on top, which is what Go or Erlang (and soon Java) do, but then you can't avoid that runtime overhead, which Rust and C++ don't want to have, or maybe you want a more lightweight model (which Javascript wanted).
In that case, the other approach you can do is explicitly represent the processes' variables and state as a structure in the heap, and create functions that operate on those structures. In Javascript you can easily do this (you can capture the values in a closure), and in Rust it is more complicated but doable, and in general you can do this in most languages.
The downside here is that, instead of being able to simply code your process in a straightforward way and have it block implicitly, you need to explicitly code the synchronous steps between each asynchronous wait, which results in either callback hell, confusing functional styles such as continuation passing, or future combinators and callbacks.
What async lets you do, then, is have the compiler write those state machines for you. If you mark your function as async, the compiler will create the closure for you, the state transitions for you, deal with waiting for nested futures for you, and so on, and you can just write the code in the 'dumb' synchronous style. This is common to all the async implementations, from Rust to JS to C#.
However, at some point, you need to actually drive these state machines. You need some kind of scheduling that is able to receive external events, get the state machine that is waiting for it, and execute the next state transition. Driving these state machines is done using those waiters and pollers. Rust exposes these APIs, because it does not bundle the executor, and so it exposes those methods so that async runtimes like tokio can be built against a standard API. Javascript, on the other hand, still needs to perform those tasks but, because it has a runtime of its own, implements an execution service implicitly, and so the external interface of Promise does not need to expose that concern.
- flipper88 4y agoa minor point, but perhaps coroutines, stackful or not, should be distinguished from threads.
- deleted 4y ago[deleted]
- olvy0 4y agoThat's a great explanation, thank you!
- samsquire 4y agoThank you for your effort and time for explaining this to me. I still need to learn more. I think I'm not sure how someone would write their own runtime with pollers and waiters. Those are the only APIs provided by Rust.