Futures and the Async Syntax
Like other languages with async, Rust uses the async and await
keywords—though with some important differences from how other languages do
things, as we will see. Blocks and functions can be marked async, and you can
wait on the result of an async function or block to resolve using the await
keyword.
Let’s write our first async function, and call it:
fn main() { hello_async(); } async fn hello_async() { println!("Hello, async!"); }
If we compile and run this… nothing happens, and we get a compiler warning:
$ cargo run
warning: unused implementer of `Future` that must be used
--> src/main.rs:2:5
|
2 | hello_async();
| ^^^^^^^^^^^^^
|
= note: futures do nothing unless you `.await` or poll them
= note: `#[warn(unused_must_use)]` on by default
warning: `hello-async` (bin "hello-async") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/hello-async`
The warning tells us that just calling hello_async() was not enough: we also
need to .await or poll the future it returns. This raises two important
questions:
- Given there is no return type on the function, how is it returning a future?
- What is a future?
Async functions
In Rust, async fn is equivalent to writing a function which returns a future
of the return type, using the impl Trait syntax we discussed back in the
“Traits as Parameters” section in Chapter 10. An async block
compiles to an anonymous struct which implements the Future trait.
That means these two are roughly equivalent:
#![allow(unused)] fn main() { async fn hello_async() { println!("Hello, async!"); } }
#![allow(unused)] fn main() { fn hello_async() -> impl Future<Output = ()> { async { println!("Hello, async!"); } } }
That explains why we got the unused_must_use warning: writing async fn meant
we were actually returning an anonymous Future. The compiler will warn us that
“futures do nothing unless you .await or poll them”. That is, futures are
lazy: they don’t do anything until you ask them to.
The compiler is telling us that ignoring a Future makes it completely useless!
This is different from the behavior we saw when using thread::spawn in the
previous chapter, and it is different from how many other languages approach
async. This allows Rust to avoid running async code unless it is actually
needed. We will see why that is later on. For now, let’s start by awaiting the
future returned by hello_async to actually have it run.
Note: Rust’s
awaitkeyword goes after the expression you are awaiting—that is, it is a postfix keyword. This is different from what you might be used to if you have used async in languages like JavaScript or C#. Rust chose this because it makes chains of async and non-async methods much nicer to work with. As of now,awaitis the only postfix keyword in the language.
fn main() { hello_async().await; }
Oh no! We have gone from a compiler warning to an actual error:
error[E0728]: `await` is only allowed inside `async` functions and blocks
--> src/main.rs:2:19
|
1 | fn main() {
| ---- this is not `async`
2 | hello_async().await;
| ^^^^^ only allowed inside `async` functions and blocks
This time, the compiler is informing us we cannot use .await in main,
because main is not an async function. That is because async code needs a
runtime: a Rust crate which manages the details of executing asynchronous
code.
Most languages which support async, including C#, JavaScript, Go, Kotlin, Erlang, and Swift, bundle a runtime with the language. At least for now, Rust does not. Instead, there are many different async runtimes available, each of which makes different tradeoffs suitable to the use case they target. For example, a high-throughput web server with dozens of CPU cores and terabytes of RAM has very different different needs than a microcontroller with a single core, one gigabyte of RAM, and no ability to do heap allocations.
To keep this chapter focused on learning async, rather than juggling parts of
the ecosystem, we have created the trpl crate (trpl is short for “The Rust
Programming Language”). It re-exports all the types, traits, and functions you
will need, and in a couple cases wires up a few things for you which are less
relevant to the subject of the book. There is no magic involved, though! If you
want to understand what the crate does, we encourage you to check out its
source code. You will be able to see what crate each re-export
comes from, and we have left extensive comments explaining what the handful of
helper functions we supply are doing.
The
futuresandtokioCratesWhenever you see code from the
trplcrate throughout the rest of the chapter, it will be re-exporting code from thefuturesandtokiocrates.
The
futurescrate is an official home for Rust experimentation for async code, and is actually where theFuturetype was originally designed.Tokio is the most widely used async runtime in Rust today, especially (but not only!) for web applications. There are other great options out there, too, and they may be more suitable for your purposes. We are using Tokio because it is the most widely-used runtime—not as a judgment call on whether it is the best runtime!
For now, go ahead and add the trpl crate to your hello-async project:
$ cargo add trpl
Then, in our main function, let’s wrap the call to hello_async with the
trpl::block_on function, which takes in a Future and runs it until it
completes.
fn main() { trpl::block_on(hello_async()); } async fn hello_async() { println!("Hello, async!"); }
When we run this, we get the behavior we might have expected initially:
$ cargo run
Compiling hello-async v0.1.0 (/Users/chris/dev/chriskrycho/async-trpl-fun/hello-async)
Finished dev [unoptimized + debuginfo] target(s) in 4.89s
Running `target/debug/hello-async`
Hello, async!
Phew: we finally have some working async code! Now we can answer that second
question: what is a future anyway? That will also help us understand why we need
that trpl::block_on call to make this work.
Futures
A future is a data structure which represents the state of some async
operation. More precisely, a Rust Future is a trait; it allows many different
data structures to represent different async operations in different ways, but
with a common interface. Here is the definition of the trait:
#![allow(unused)] fn main() { pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
Future has an associated type, Output, which says what the result of the
future will be when it resolves. (This is analogous to the Item associated
type for the Iterator trait, which we saw back in Chapter 13.) Beyond that,
Future has only one method: poll, which takes a special Pin reference for
its self parameter and a mutable reference to some Context type, and returns
a Poll<Self::Output>. We will talk a little more about Pin and Context
later in the chapter. For now, let’s focus on what the method returns, the
Poll type:
#![allow(unused)] fn main() { enum Poll<T> { Ready(T), Pending } }
You may notice that this Poll type is a lot like an Option. Having a
dedicated type lets Rust treat Poll differently from Option, though, which
is important since they have very different meanings! The Pending variant
indicates that the future still has work to do, so the caller will need to check
again later. The Ready variant indicates that the Future has finished its
work and the T value is available.
Note: With most futures, the caller should not call
poll()again after the future has returnedReady. Many futures will panic if polled after becoming ready! Futures which are safe to poll again will say so explicitly in their documentation.
Under the hood, when you call .await, Rust compiles that to code which calls
poll, kind of like this:
match hello_async().poll() {
Ready(_) => {
// We’re done!
}
Pending => {
// But what goes here?
}
}
As you can see from this sample, though, there is a question: what happens when
the Future is still Pending? We need some way to try again. We would need to
have something like this instead:
let hello_async_fut = hello_async();
loop {
match hello_async_fut.poll() {
Ready(_) => {
break;
}
Pending => {
// continue
}
}
}
When we use .await, Rust actually does compile it to something very similar to
that loop. If Rust compiled it to exactly that code, though, every .await
would block the computer from doing anything else—the opposite of what we were
going for! Instead, Rust internally makes sure that the loop can hand back
control to the the context of the code where which is awaiting this little bit
of code.
When we follow that chain far enough, eventually we end up back in some
non-async function. At that point, something needs to “translate” between the
async and sync worlds. That “something” is the runtime! Whatever runtime you use
is what handles the top-level poll() call, scheduling and handing off between
the different async operations which may be in flight, and often also providing
async versions of functionality like file I/O.
Now we can understand why the compiler was stopping us in Listing 17-2 (before
we added the trpl::block_on function). The main function is not async—and
it really cannot be: if it were, something would need to call poll() on
whatever main returned! Instead, we use the trpl::block_on function, which
polls the Future returned by hello_async until it returns Ready. Every
async program in Rust has at least one place where it sets up an executor and
executes code.
Note: Under the hood, Rust uses generators so that it can hand off control between different functions. These are an implementation detail, though, and you never have to think about it when writing Rust.
The loop as written also wouldn’t compile, because it doesn’t actually satisfy the contract for a
Future. In particular,hello_async_futis not pinned with thePintype and we did not pass along aContextargument.More details here are beyond the scope of this book, but are well worth digging into if you want to understand how things work “under the hood.” In particular, see Chapter 2: Under the Hood: Executing Futures and Tasks and Chapter 4: Pinning in the official Asynchronous Programming in Rust book.
Now, that’s a lot of work to just print a string, but we have laid some key foundations for working with async in Rust! Now that you know the basics of how futures and runtimes work, we can see some of the things we can do with async.