This content originally appeared on DEV Community and was authored by DEV Community
TL;DR: I will try to give an easy-to-understand account of some of concepts surrounding asynchronous Rust: async, await, Future, Poll, Context, Waker, Executor and Reactor.
As with most things I write here, we already have good content related to asynchronous Rust. Let me mention a few:
- The Asynchronous Programming in Rust, a.k.a. async book; incomplete, but great.
- Steve's talks on Rust's Journey to async/await and on how it works.
- Without Boats' proposal for await syntax (the other entries with the tags
async
andFuture
are also excellent). - Jon's stream on how Futures and async/await works.
With this amount of superb information, why writing about it? My answer here is the same for almost every other entry on my DEV blog: to reach an audience for which this content is still a bit too hard to grasp.
So, if you want something in a more intermediary level, go straight to the content listed above. Otherwise, let's go :)
async/.await
Asynchronous Rust (async Rust, for short) is delivered through the async/.await
syntax. It means that these two keywords (async
and .await
) are the centerpieces of writing async Rust. But what is async Rust?
The async book states that async is a concurrent programming model. Concurrent means that different tasks will perform their activities alternatively; e.g., task A does a bit of work, hands the thread over to task B, who works a little and give it back, etc.
Do not confuse it with parallel programming, where different tasks are running simultaneously.
In short, we use the async
keyword to tell Rust that a block or a function is going to be asynchronous.
// asynchronous block
async {
// ...
}
// asynchronous function
async fn foo(){
// ...
}
But what does it mean for a Rust program to be asynchronous? It means that it will return an implementation of the Future
trait. I will cover Future
in the next section; for now, it is enough to say that a Future
represents a value that may or may not be ready.
We handle a Future
that is returned by an async block/function with the .await
keyword. Consider the silly example below:
async fn foo() -> i32 {
11
}
fn bar() {
let x = foo();
// it is possible to .await only inside async fn or block
async {
let y = foo().await;
};
}
In this case, x
is not i32
, but the implementation of the Future
trait (impl Future<Output = i32>
in this case). The variable y
on the other hand, will be a i32
: 11.
Other way to visualize this is to understand that Rust will desugar this
async fn foo() -> i32 {}
into something like this
fn foo() -> impl Future<Output=i32>{}
Of course, there is no asynchronous anything happening here. But if foo()
was complex, having to wait for Mutex
locks or a stream, instead of holding the thread for the whole time, Rust would do as much progress as possible on foo()
and then release the thread to do something else, taking it back when it could do more work.
Hopefully, it will make sense after we go through concepts like Future
, Poll
and Wake
. For now, it is enough that you have a general idea of the use of both async
and await
.
Be sure to read the async/.await Primer.
Futures
I think it is not an exaggeration to say that the Future
trait is the heart of async Rust.
A Future
is a trait that has:
- An
Output
type (i32
in the example above). - A
poll
function.
poll()
is a function that does as much work as it can, and then returns an enum called Poll
:
enum Poll<T> {
Ready(T),
Pending,
}
This enum is the representation of what I wrote earlier, that a Future represents a value that may or may not be ready.
The general idea behind this function is simple: when someone calls poll()
on a future, if it went all the way through completion, it returns Ready(T)
and the .await
will return T
. Otherwise, it will return Pending
.
The question is, if it returns Pending
, how do we get back at it, so it can keep working towards completion? The short answer is the reactor. However, we have some ground to cover before getting there.
Poll, Context, Waker, Executor and Reactor
Lots of words! But I honestly think it is easier to bundle everything together because it is easier to understand what they do in context. And to illustrate this, I came up with a simplified hypothetical scenario.
Suppose we have a Future
created via async
keyword. Let's remember what a Future is:
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
As hinted by the code above, futures in Rust are lazy, which means that just declaring them will not make them run.
Now, let's say we run the future using .await
. "Run" here means delivering it to an executor that will call poll()
in the future.
I will not cover
Pin
here, as it is somewhat complex and not necessary to understand what is going on here.
If poll()
returns Ready
, the executor will get rid of it.
Alternatively, if the polled future wasn't able to do all the work, it will return Pending
.
After receiving Pending
, the executor will not poll the future again until it is told so. And who is going to tell him? The reactor. It will call the wake()
function on the Waker
that was passed as an argument in the poll()
function. That allows the executor to know that the associated task is ready to move on.
When we talk about executor and reactor we are already talking about runtimes; and when we talk about runtimes we are usually talking about Tokio. In fact, calling it by the names executor and reactor is already adhering to Tokio nomenclatures.
Regarding the executor, what Tokio does is more or less what I have described above. When it comes to the reactor, complexity grows exponentially. And the reason is that the reactor is some sort of "interface" between the future and some I/O. Jon spent 45 minutes explaining this while drawing on a blackboard, and I will not pretend I can do a better job. So, if you want to dive into this level of detail, check the link above.
Wrapping up
Let us recap:
-
async
is used to create an asynchronous block or function, making it return aFuture
. -
.await
will wait for the completion of the future and then give back the value (or an error, which is why it is common to use the question mark operator in.await?
). -
Future
is the representation of an asynchronous computation, a value that may or may not be ready, something that is represented by the variants of thePoll
enum. -
Poll
is the enum returned by a future, whose variants can be eitherReady<T>
orPending
. -
poll()
is the function that works the future towards its completion. It receives aContext
as a parameter and is called by the executor. -
Context
is a wrapper forWaker
. -
Waker
is a type that contains awake()
function that will be called by the reactor, telling the executor that it may poll the future again. -
Executor is a scheduler that executes the futures by calling
poll()
repeatedly. - Reactor is something like an event loop responsible for waking up the pending futures.
Ok, there is certainly more to talk about, such as the Send
and Sync
traits, Pinning
and so on, but I think that, for a beginner post, we had enough.
See you next time!
Cover art by TK.
This content originally appeared on DEV Community and was authored by DEV Community
DEV Community | Sciencx (2021-08-29T20:56:01+00:00) Asynchronous Rust: basic concepts. Retrieved from https://www.scien.cx/2021/08/29/asynchronous-rust-basic-concepts/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.