4 ms·
That's the part that is difficult: Efficient wake up on completion. Network I/O is usually monitored via a so called I/O loop. Local file I/O are often not exp
by bombela 6y ago
That's the part that is difficult: Efficient wake up on completion.
Network I/O is usually monitored via a so called I/O loop. Local file I/O are often not exposed with asynchronous APIs (yes I know it's changing on linux quickly). But what about locks? Let's say you want a tread safe queue, how do you wake up the right function when the mutex is unlocked? I suspect this is the reason of all this complexity.
- Someone 6y agoI still don’t understand. If you want wake-up on completion, you create a thread/green thread/task/coroutine (whatever fits your bill) and have it await the future (likely also make the call that creates it) What do you get by doing that inside the future machinery? Maybe fewer tasks and their call stacks, but is that a huge problem? Also, what’s with wake and poll? https://doc.rust-lang.org/std/future/trait.Future.html https://doc.rust-lang.org/std/future/trait.Future.html: “Futures alone are inert; they must be actively polled to make progress, meaning that each time the current task is woken up, it should actively re-poll pending futures that it still has an interest in. The poll function is not called repeatedly in a tight loop -- instead, it should only be called when the future indicates that it is ready to make progress (by calling wake())” So, an asynchronous task that doesn’t have an answer yet calls wake to tell some object that it has to call poll on the task, so that the asynchronous task can make more progress? Why doesn’t it just do what it was asked to do until it has an answer? Also, if a future is inert, how can it ever detect that it is ready to make progress and call wake? I really do not understand this design. I guess a UML sequence diagram (https://en.mwikipedia.org/wiki/Sequence_diagram https://en.mwikipedia.org/wiki/Sequence_diagram) would help me. It would show what entities exist (if Rust’s future is inert, it can’t be much more than the memory block holding the function result and a reference to the async code. If so, what’s doing the computation?) put names on them, and show what calls they make.
- steveklabnik 6y agoI gave a pair of talks on the background and history here. They may help? Transcripts + video: * https://www.infoq.com/presentations/rust-2019/ https://www.infoq.com/presentations/rust-2019/ * https://www.infoq.com/presentations/rust-async-await/ https://www.infoq.com/presentations/rust-async-await/ I'd be re-writing what's in there, so check those out and if it's still confusing, I'll check back in this thread (there's a joke here somewhere...) later.
- rustshellscript 6y agoThanks Steve. I have studied the "200 lines" book a few days ago, but was still confused. Only after seeing your talks, everything becomes so much clearer. These talks should really be part of the "async book" :)
- steveklabnik 6y agoThe code in the second one is explaining the code in the async book! So it sorta kinda is.
- millstone 6y ago> if a future is inert, how can it ever detect that it is ready to make progress and call wake This is the crux, and I wish the discussion started by posing and answering this question. Futures are NOT inert. This is referenced only obliquely: When a future is not ready yet, poll returns Poll::Pending and stores a clone of the Waker copied from the current Context. This Waker is then woken once the future can make progress The passive voice "the waker is woken" obscures the fact that the Future itself is responsible for arranging the call to wake(). This means that the Future maintains some sort of dual active presence: it owns a thread, or a timer, or adds a file descriptor to an fd set, etc. An example from futures::io::AsyncRead::poll: If no data is available for reading, the method returns Poll::Pending and arranges for the current task to receive a notification when the object becomes readable or is closed. So `poll()` is a misleading name: it is not a passive check but a hook for the Future to materialize its dual, active half.
- Arnavion 6y agoThe waking is done by the executor. You can call the executor the "dual active presence" of the future or whatever if you want. I think it would be misleading because there's only one executor and there are many futures.
- jkarneges 6y agoI believe the rationale goes like this: Rust doesn't have a runtime, and the designers prefer to to keep it that way. But, there was also a desire for native-feeling async programming in Rust. In order to reconcile these seemingly opposing stances, an abstract Future trait was added to the standard library with no implementation, along with async and await keywords to operate with it. This way, an async runtime could live outside of the standard library, but developers would still get to use nice built-in keywords. It's worth noting that despite there not being an async runtime shipped with Rust, the designers have taken an opinionated stance on the use of "stackless" coroutines. The await keyword is only allowed inside of a function/block declared async, and suchs blocks always get rewritten by the compiler. The end result is truly impressive though. Async Rust is more efficient CPU-wise and memory-wise than anything I've ever heard of. So while async Rust might be difficult to understand and use at times, it's this way in the name of efficiency (good description of Rust in general :)). I did some benching to compare hand written poll loop code to async code here: https://github.com/jkarneges/rust-async-bench https://github.com/jkarneges/rust-async-bench
- ryani 6y agoSo the goal of futures is to deal with inversion of control. The code you want to write is something like: fn f( filename ) { val someUrl = computeTheUrl(); val data = await http.get( someUrl ); val processedData = process(Data); await filesystem.writeFile( filename, processedData ); } (I'm marking "potentially blocking" calls with "await" here) But the code you want to run is an event loop: while( program_is_running() ) { poll_io() poll_network() run_ready_tasks() } So can you do to let you write f()? You need to invert the control between run_ready_tasks() and f(). And you need to do so in a way that allows f() to both do network io, disk io, and other blocking tasks. You're right that you can use a green thread per task to solve this problem, and "await" is then just a function that switches context to the scheduler. For various reasons, the Rust community decided that green threads were too heavy of an abstraction, and instead the compiler generates state machines (which are, in some ways, equivalent to green threads if you squint hard enough). > if a future is inert, how can it ever detect that it is ready to make progress and call wake? The future doesn't. When the future makes a blocking call, it needs to push its waker into some data structure handled by the top level event loop / scheduler. And then the job of the waker is to understand how to push the task back into the ready queue when whatever was being waited on completes. The complications in Rust's design is due to their goal of making it so those compiler-generated-state-machines can be used by a library runtime -- to not require a particular standard library.