I recently spent 15 minutes going over a Rust futures tutorial and was then able to get a Rust program streaming live bitcoin futures (coincidence!) over websocket connections from multiple exchanges working in less than an hour.
I then then attempted to port some Python code to use Python's new async/await and was unsuccessful. I managed to get a bunch of async workers running, but only the first one would process jobs from the queue or the entire program would just hang indefinitely. Couldn't figure it out after two hours and just gave up on it.
Nice work, Rust team. This is a game changer for me.
Looking at the code, it's a nightmare to parse. There are so many details to deal with that should never bother you under normal circumstances (the core problem of using Rust where it's not necessary). It's admirable that you have such an easy time with it. However, for most people, using Rust for these purposes is absolutely not recommended.
Java/Kotlin paired with reactive streams makes these tasks super easy to read, once you get the hang of reactive streams. Also there are complexities in there, you just simply can't brush all the intricacies of network programming and multi-threading under the rug, but at least you are dealing only with the complexities that are necessary for your problem.
With Rust you are dealing with tons of additional complexity that doesn't matter to 99% of people & problems. I love that Rust exists and it has its uses (browsers, hypervisors, drivers, kernels, etc). I just find it very alienating when people compare this to Python and do everyday tasks with it.
No, you really don't want to deal with borrowing, memory management and life-times, unless it is part of the problem you are trying to solve (and there aren't too many for which that applies).
Yes, this is usually how it turns out in practice. You don't need to understand what's going on under the hood, you just string some libraries together and call it a day ;).
> However, for most people, using Rust for these purposes is absolutely not recommended.
Who are these "most people"? Can you get more specific here?
> Java/Kotlin paired with reactive streams makes these tasks super easy to read, once you get the hang of reactive streams. Also there are complexities in there, you just simply can't brush all the intricacies of network programming and multi-threading under the rug, but at least you are dealing only with the complexities that are necessary for your problem.
The text goes pretty much into detail. I do not understand where Rusts streams are more complicated than the Java/Kotlin/Scala ones? Did you
ever write an application with reactive streams and got problems with the thread pool, people unknowingly blocking in combinators, etc..?
I'd dare to say that these things also add complexity that is not necessary to the kind of problems you defined as "99% of ..."
> With Rust you are dealing with tons of additional complexity that doesn't matter to 99% of people & problems.
Where did you get that number from? What are "tons of additional complexity"?
> I just find it very alienating when people compare this to Python and do everyday tasks with it.
Maybe you just have not yet recognized that you can do everyday tasks with it?
This article is explaining how Futures work by showing you what's under the covers. You don't actually have to write this code yourself, you can just use one of many wonderful libraries that handle it all for you.
As it says in the second paragraph: "Going into the level of detail I do in this book is not needed to use futures or async/await in Rust. It's for the curious out there that want to know how it all works."
That’s more “Futures in Rust explained in 200 lines (of Rust)”.
Explaining futures doesn’t require 200 lines: when a function doesn’t return what you asked for, but a ticket that you can later use to check whether what you asked for is available or exchange for what you asked for, that ticket is called “a future”. Functions returning futures allow you to do some other work while the function does what you asked it to do.
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.
I 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?
“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) 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.
I 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 :)).
> 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.
The 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.
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.
Thanks 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" :)
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.
> but a ticket that you can later use to check whether what you asked for is available or exchange for what you asked for, that ticket is called “a future”
The whole original point of futures was that there was no ticket and no checking - when you used the value it blocked.
I think what's described here is more like what was originally called an eventual value.
But most people seem to have forgotten these original ideas and definitions.
I then then attempted to port some Python code to use Python's new async/await and was unsuccessful. I managed to get a bunch of async workers running, but only the first one would process jobs from the queue or the entire program would just hang indefinitely. Couldn't figure it out after two hours and just gave up on it.
Nice work, Rust team. This is a game changer for me.