3 ms·
There's roughly three pieces to async/await. The first piece is the compiler support for the keywords, to do the state machine transformation for you. This can
by jcranmer 3y ago
There's roughly three pieces to async/await.
The first piece is the compiler support for the keywords, to do the state machine transformation for you. This can only be done by the compiler, so it's agnostic of the runtime. This support also requires some ancillary standard library support for what an async task is, to know what the result of the state machine transformation looks like. In pseudocode, this is:
do_async_task(wake) -> Poll<result>:
if !ready_to_read:
schedule a call to wake when ready to read
return Poll::not_ready
return compute(read())
The second piece of the puzzle is the top-level code to drive the polling. This is what's commonly thought of as the runtime; you generally have a top-level event loop, and asking to wake means injecting a new event in the event loop that will call the async task again.
The third piece of the puzzle is the "schedule a call" part, essentially this is the code that works with low-level system calls like epoll or io_uring or IOCP or boring old select. Except, as you immediately see if you have experience with such system calls, writing that code requires that the top-level event loop essentially be consistently triggering the call to check for new work. So this piece of puzzle, especially for I/O, generally needs to be intimately connected to the top-level executor to work well.
What could the standard library do (or have done) to make things work better? The obvious thing is standardize basic async concepts like AsyncRead or async variant of iterators, except the difficulty with that for I/O in particular is that the library would be standardizing interfaces without providing implementations. Less obvious is baking in a standard I/O event loop interface--a standard way that would allow adding new events to the runtime's main epoll or whatever interface. However, it sort of turns out that many OSes don't actually provide nice interfaces for "wait for I/O or timer or child process status change or GUI event or ..." which is what you really want to have.
- Groxx 3y agoCouldn't that "schedule a call to wake when ready to read" be "it's just another future that does whatever it needs"? Whether that's "have the shared epoll-poller check every millisecond via a timer and resolve the relevant futures that it is observing" or "wait on a blocked thread" doesn't seem like it matters. It needs checking either constantly (hot loop), after a period, or it'll be externally resolved and that resolver will let the event loop know something's ready, and... those seem rather straightforward to label and support. I can definitely see why an enforced-shared tightly-integrated epoll-er has implicit performance benefits, but that hardly seems necessary either. And stuff chasing the last bits of performance basically always give up some interoperability.
- jcranmer 3y agoIf you're deferring to another future, that's the first piece of the puzzle (which is primarily "solved" by having the compiler do some magic to make the deferring easy, although things like the futures crate also provides a lot of useful interfaces for async I/O without actual implementations). At the end of the day, there is some fundamental future that has to implement the "schedule a call" piece, and that requires some degree of coordination with the top-level executor.
- Groxx 3y agoCoordination as an API, sure. We have ways to make that extensible though, why do they not work here? "External wake or check later" seems entirely feasible and not at all "intimately connected".