Chapter 17
Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams
Many operations take a while to finish; asynchronous programming lets us express code as potential pausing points and eventual results. Building on Chapter 16’s threads, this chapter introduces Rust’s futures, streams, the async and await syntax, and the crates that implement async runtimes.
- Parallelism and Concurrency
- 17.1 Futures and the Async Syntax
- 17.2 Applying Concurrency with Async
- 17.3 Working with Any Number of Futures
- 17.4 Streams: Futures in Sequence
- 17.5 A Closer Look at the Traits for Async
- 17.6 Putting It All Together: Futures, Tasks, and Threads
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
1
Chapter 17 · Introduction
Doing other work while we wait
Many operations we ask the computer to do can take a while to finish. It would be nice to do something else while we’re waiting for those long-running processes to complete. Modern computers offer two techniques for working on more than one operation at a time: parallelism and concurrency.
Our programs’ logic, however, is written in a mostly linear fashion. We’d like to specify the operations a program should perform and the points at which a function could pause and some other part of the program could run instead — without specifying up front exactly the order and manner in which each bit of code should run.
Definition 17.1 (Asynchronous programming). An abstraction that lets us express our code in terms of potential pausing points and eventual results, and that takes care of the details of coordination for us.
This chapter builds on Chapter 16’s use of threads for parallelism and concurrency by introducing an alternative approach: Rust’s futures, streams, and the async and await syntax that let us express how operations could be asynchronous, and the third-party crates that implement asynchronous runtimes — code that manages and coordinates the execution of asynchronous operations.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
2
Chapter 17 · Introduction
Two long-running operations
Exporting a video
Say you’re exporting a video you’ve created of a family celebration, an operation that could take anywhere from minutes to hours. The export will use as much CPU and GPU power as it can. If you had only one CPU core and your operating system didn’t pause that export until it completed — that is, if it executed the export synchronously — you couldn’t do anything else on your computer while it ran. Fortunately, the operating system can, and does, invisibly interrupt the export often enough to let you get other work done simultaneously.
Downloading a video
Now say you’re downloading a video shared by someone else. This can also take a while but does not take up as much CPU time: the CPU has to wait for data to arrive from the network. You can start reading the data once it starts to arrive, but it might take some time for all of it to show up — and even then, loading a large video could take a second or two. That is a very long time for a modern processor, which can perform billions of operations every second. Again, the OS invisibly interrupts your program so the CPU can do other work while the network call finishes.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
3
Chapter 17 · Introduction
CPU-bound and I/O-bound operations
Definition 17.2 (CPU-bound or compute-bound operation). An operation limited by the computer’s potential data processing speed within the CPU or GPU, and by how much of that speed it can dedicate to the operation. The video export is one.
Definition 17.3 (I/O-bound operation). An operation limited by the speed of the computer’s input and output: it can only go as fast as the data can be sent across the network. The video download is one.
In both examples, the operating system’s invisible interrupts provide a form of concurrency. That concurrency happens only at the level of the entire program, though: the operating system interrupts one program to let other programs get work done.
In many cases, because we understand our programs at a much more granular level than the operating system does, we can spot opportunities for concurrency that the operating system can’t see.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
4
Chapter 17 · Introduction
Blocking calls
If we’re building a tool to manage file downloads, we should be able to write our program so that starting one download won’t lock up the UI, and users should be able to start multiple downloads at the same time. Many operating system APIs for interacting with the network are blocking, though.
Definition 17.4 (Blocking). A blocking call blocks the program’s progress until the data it’s processing is completely ready.
Remark. This is how most function calls work, if you think about it. However, the term blocking is usually reserved for function calls that interact with files, the network, or other resources on the computer, because those are the cases where an individual program would benefit from the operation being non-blocking.
We could avoid blocking our main thread by spawning a dedicated thread to download each file. However, the overhead of the system resources used by those threads would eventually become a problem. It would be preferable if the call didn’t block in the first place: instead, we could define a number of tasks we’d like our program to complete and allow the runtime to choose the best order and manner in which to run them.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
5
Chapter 17 · Introduction
What async gives us, and what this chapter covers
Defining tasks and letting the runtime choose how to run them is exactly what Rust’s async (short for asynchronous) abstraction gives us. In this chapter you’ll learn:
- how to use Rust’s async and await syntax and execute asynchronous functions with a runtime;
- how to use the async model to solve some of the same challenges we looked at in Chapter 16;
- how multithreading and async provide complementary solutions that you can combine in many cases.
Before we see how async works in practice, though, we take a short detour to discuss the differences between parallelism and concurrency.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
6
Chapter 17 · Parallelism and Concurrency
Concurrency: one person, several tasks
So far we’ve treated parallelism and concurrency as mostly interchangeable; now we need to distinguish them precisely, because the differences will show up as we work. Consider how a team could split up work on a software project: assign a single member multiple tasks, assign each member one task, or use a mix of the two.
Definition 17.5 (Concurrency). An individual works on several different tasks before any of them is complete.
One way to implement concurrency is like having two different projects checked out on your computer: when you get bored or stuck on one, you switch to the other. You’re just one person, so you can’t make progress on both at the exact same time, but you can multitask, making progress on one at a time by switching between them.
Figure 17.1: A concurrent workflow, switching between Task A and Task B (time runs left to right)
Task A
A1
A2
A3
A4
Task B
B1
B2
B3
Order of work: A1 → B1 → A2 → B2 → A3 → A4 → B3 — one worker, switching between the tasks.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
7
Chapter 17 · Parallelism and Concurrency
Parallelism: one task per person
When the team splits up a group of tasks by having each member take one task and work on it alone, this is parallelism. Each person on the team can make progress at the exact same time.
Definition 17.6 (Parallelism). Each worker takes one task and works on it alone, so progress on the different tasks happens at the exact same time.
Figure 17.2: A parallel workflow, where work happens on Task A and Task B independently
Task A
A1
A2
A3
A4
Task B
B1
B2
B3
A1 → A2 → A3 → A4 and B1 → B2 → B3 proceed at the same time; nothing passes between the two tasks.
Compare Figure 17.1: there, a single worker alternated between the tasks; here, two workers each own one task from start to finish.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
8
Chapter 17 · Parallelism and Concurrency
When work becomes serial
In both workflows, you might have to coordinate between tasks. Maybe you thought the task assigned to one person was totally independent from everyone else’s work, but it actually requires another person on the team to finish their task first. Some of the work could be done in parallel, but some of it was actually serial.
Definition 17.7 (Serial). Work that can only happen in a series, one task after the other.
Figure 17.3: A partially parallel workflow, where work happens on Task A and Task B independently until Task A3 is blocked on the results of Task B3
Task A
A1
A2
‖ pause
A3
Task B
B1
B2
B3
B4
A1 → A2, then Task A pauses; B1 → B2 → B3; B3 feeds both A3 and B4.
Likewise, you might realize that one of your own tasks depends on another of your tasks. Now your concurrent work has also become serial.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
9
Chapter 17 · Parallelism and Concurrency
Where they intersect, in teams and in hardware
Parallelism and concurrency can intersect with each other, too. If you learn that a colleague is stuck until you finish one of your tasks, you’ll probably focus all your efforts on that task to “unblock” your colleague. You and your coworker are no longer able to work in parallel, and you’re also no longer able to work concurrently on your own tasks.
The same basic dynamics come into play with software and hardware:
- On a machine with a single CPU core, the CPU can perform only one operation at a time, but it can still work concurrently. Using tools such as threads, processes, and async, the computer can pause one activity and switch to others before eventually cycling back to the first.
- On a machine with multiple CPU cores, it can also do work in parallel. One core can perform one task while another core performs a completely unrelated one, and those operations actually happen at the same time.
Running async code in Rust usually happens concurrently. Depending on the hardware, the operating system, and the async runtime we are using (more on runtimes shortly), that concurrency may also use parallelism under the hood.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
10
Chapter 17 · 17.1 Futures and the Async Syntax
Futures, async and await
The key elements of asynchronous programming in Rust are futures and Rust’s async and await keywords.
Definition 17.8 (Future). A value that may not be ready now but will become ready at some point in the future. (Other languages have the same concept, sometimes called a task or a promise.) In Rust, futures are types that implement the Future trait.
Rust provides the Future trait as a building block so that different async operations can be implemented with different data structures but with a common interface. Each future holds its own information about the progress that has been made and what “ready” means.
You can apply the async keyword to blocks and functions to specify that they can be interrupted and resumed. Within an async block or async function, you can use the await keyword to await a future (that is, wait for it to become ready). Any point where you await a future within an async block or function is a potential spot for that block or function to pause and resume.
Definition 17.9 (Polling). The process of checking with a future to see if its value is available yet.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
11
Chapter 17 · 17.1 Futures and the Async Syntax
From async and await to the Future trait
Some other languages, such as C# and JavaScript, also use async and await keywords for async programming. If you know them, you may notice significant differences in how Rust handles the syntax — for good reason, as we’ll see!
When writing async Rust, we use the async and await keywords most of the time. Rust compiles them into equivalent code using the Future trait, much as it compiles for loops into equivalent code using the Iterator trait:
Table 17.1: Syntax and the trait it compiles to
| Syntax you write | Compiled into code using |
|---|---|
| for loops | the Iterator trait |
| async and await | the Future trait |
Because Rust provides the Future trait, you can also implement it for your own data types when you need to. Many functions in this chapter return types with their own implementations of Future. We return to the trait’s definition at the end of the chapter.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
12
Chapter 17 · 17.1 Futures and the Async Syntax
Our first async program: the trpl crate
To keep the focus on learning async rather than juggling parts of the ecosystem, the book provides the trpl crate (short for “The Rust Programming Language”). It re-exports all the types, traits, and functions you’ll need, primarily from two crates:
- futures — an official home for Rust experimentation for async code; the Future trait was originally designed there.
- tokio — the most widely used async runtime in Rust today, especially for web applications. Other great runtimes may suit your purposes better; trpl uses Tokio because it’s well tested and widely used.
In some cases trpl also renames or wraps the original APIs. Its source code (github.com/rust-lang/book/tree/main/packages/trpl) shows which crate each re-export comes from, with extensive comments. Create a new binary project and add trpl as a dependency:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
13
Chapter 17 · 17.1 Futures and the Async Syntax
Defining the page_title function
The goal: a little command line web scraper that fetches two web pages concurrently, pulls the <title> element from each, and prints the title of whichever page finishes that whole process first.
We start with a function that takes one page URL, makes a request to it, and returns the text of the <title> element.
Listing 17.1: Defining an async function to get the title element from an HTML page — src/main.rs
use trpl::Html;
async fn page_title(url: &str) -> Option<String> {
let response = trpl::get(url).await;
let response_text = response.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
14
Chapter 17 · 17.1 Futures and the Async Syntax
Awaiting get and text: futures are lazy
We mark page_title with the async keyword. We use trpl::get to fetch whatever URL is passed in and add await to await the response. To get the text of the response, we call its text method and once again await it. Both steps are asynchronous:
- for get, we wait for the server to send back the first part of its response, which includes HTTP headers, cookies, and so on and can be delivered separately from the response body;
- especially if the body is very large, it can take some time for it all to arrive; because we have to wait for the entirety of the response, text is also async.
We have to explicitly await both futures, because futures in Rust are lazy: they don’t do anything until you ask them to with await. (Rust even shows a compiler warning if you don’t use a future.) This mirrors iterators (Chapter 13), which do nothing unless you call their next method — directly, or via for loops or methods such as map that use next under the hood. This laziness allows Rust to avoid running async code until it’s actually needed.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
15
Chapter 17 · 17.1 Futures and the Async Syntax
Laziness vs threads; extracting the title
Remark. This is different from thread::spawn in Chapter 16, where the closure we passed to another thread started running immediately. It’s also different from how many other languages approach async. But it’s important for Rust to be able to provide its performance guarantees, just as it is with iterators.
Once we have response_text, we parse it into an instance of the Html type using Html::parse. Instead of a raw string, we now have a data type we can use to work with the HTML as a richer data structure.
- select_first finds the first instance of a given CSS selector. Passing "title" gets the first <title> element in the document, if there is one.
- Because there may be no matching element, select_first returns an Option<ElementRef>.
- Option::map lets us work with the item in the Option if it’s present, and do nothing if it isn’t. (A match expression would also work, but map is more idiomatic.)
- In the function we supply to map, we call inner_html on the title to get its content, a String. When all is said and done, we have an Option<String>.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
16
Chapter 17 · 17.1 Futures and the Async Syntax
await is a postfix keyword
Rust’s await keyword goes after the expression you’re awaiting, not before it: it’s a postfix keyword. This may differ from what you’re used to in other languages, but in Rust it makes chains of methods much nicer to work with. We can chain the trpl::get and text calls with await between them:
Listing 17.2: Chaining with the await keyword — src/main.rs
use trpl::Html;
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
With that, we have successfully written our first async function! Before we add code in main to call it, let’s talk a little more about what we’ve written and what it means.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
17
Chapter 17 · 17.1 Futures and the Async Syntax
What an async fn compiles to
Rust compiles an async block into a unique, anonymous data type that implements the Future trait. It compiles an async function into a non-async function whose body is an async block; the function’s return type is the type of that anonymous data type.
So writing async fn is equivalent to writing a function that returns a future of the return type. To the compiler, async fn page_title in Listing 17.1 is roughly equivalent to:
use std::future::Future;
use trpl::Html;
fn page_title(url: &str) -> impl Future<Output = Option<String>> {
async move {
let text = trpl::get(url).await.text().await;
Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html())
}
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
18
Chapter 17 · 17.1 Futures and the Async Syntax
Walking through the transformed version
- It uses the impl Trait syntax from Chapter 10’s “Traits as Parameters” section.
- The returned value implements the Future trait with an associated type of Output. The Output type is Option<String>, the same as the original return type of the async fn version of page_title.
- All of the code called in the body of the original function is wrapped in an async move block. Remember that blocks are expressions: this whole block is the expression returned from the function.
- This async block produces a value with the type Option<String>, which matches the Output type in the return type — just like other blocks you have seen.
- The new function body is an async move block because of how it uses the url parameter. (async versus async move is discussed much more later in the chapter.)
Now we can call page_title in main.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
19
Chapter 17 · 17.1 Futures and the Async Syntax
Calling page_title from main (does not compile yet)
Listing 17.3: Calling the page_title function from main with a user-supplied argument — src/main.rs
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
20
Chapter 17 · 17.1 Futures and the Async Syntax
main is not allowed to be async
Listing 17.3 tries to get the title for a single page. It follows the pattern for getting command line arguments from Chapter 12’s “Accepting Command Line Arguments” section, passes the URL argument to page_title, and awaits the result. Because the value produced by the future is an Option<String>, a match expression prints different messages depending on whether the page had a <title>.
Unfortunately, this code doesn’t compile. The only place we can use the await keyword is in async functions or blocks, and Rust won’t let us mark the special main function as async:
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
21
Chapter 17 · 17.1 Futures and the Async Syntax
Async code needs a runtime
The reason main can’t be marked async is that async code needs a runtime.
Definition 17.10 (Runtime). A Rust crate that manages the details of executing asynchronous code.
A program’s main function can initialize a runtime, but it’s not a runtime itself (we’ll see why in a bit). Every Rust program that executes async code has at least one place where it sets up a runtime that executes the futures.
Most languages that support async bundle a runtime, but Rust does not. Instead, there are many different async runtimes available, each of which makes different tradeoffs suitable to the use case it targets. For example, a high-throughput web server with many CPU cores and a large amount of RAM has very different needs than a microcontroller with a single core, a small amount of RAM, and no heap allocation ability.
The crates that provide those runtimes also often supply async versions of common functionality such as file or network I/O.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
22
Chapter 17 · 17.1 Futures and the Async Syntax
Running a future with trpl::block_on
Here, and throughout the rest of this chapter, we use the block_on function from the trpl crate:
- It takes a future as an argument and blocks the current thread until this future runs to completion.
- Behind the scenes, calling block_on sets up a runtime using the tokio crate, which is used to run the future passed in. (trpl’s block_on behaves similarly to other runtime crates’ block_on functions.)
- Once the future completes, block_on returns whatever value the future produced.
We could pass the future returned by page_title directly to block_on and, once it completed, match on the resulting Option<String> as we tried to do in Listing 17.3. However, for most of the examples in the chapter (and most async code in the real world), we’ll be doing more than just one async function call. So instead we pass an async block and explicitly await the result of the page_title call, as in Listing 17.4.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
23
Chapter 17 · 17.1 Futures and the Async Syntax
Awaiting an async block inside block_on
Listing 17.4: Awaiting an async block with trpl::block_on — src/main.rs
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
main stays an ordinary function; the async block passed to trpl::block_on is where await is allowed. page_title follows on the next slide.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
24
Chapter 17 · 17.1 Futures and the Async Syntax
Running the single-page version
Listing 17.4 (continued) — src/main.rs
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
When we run this code, we get the behavior we expected initially:
$ cargo run -- "https://www.rust-lang.org"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
25
Chapter 17 · 17.1 Futures and the Async Syntax
Await points and the hidden state machine
Phew — we finally have some working async code! Before we race two sites against each other, let’s turn back briefly to how futures work.
Definition 17.11 (Await point). Any place where the code uses the await keyword. Each await point represents a place where control is handed back to the runtime.
To make that work, Rust needs to keep track of the state involved in the async block, so that the runtime could kick off some other work and then come back when it’s ready to try advancing the first one again. This is an invisible state machine, as if you’d written an enum like this to save the current state at each await point:
enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
26
Chapter 17 · 17.1 Futures and the Async Syntax
Who runs the state machine: runtimes and executors
Writing the state transitions by hand would be tedious and error-prone, especially as you add more functionality and states later. Fortunately, the compiler creates and manages the state machine data structures for async code automatically. The normal borrowing and ownership rules still apply, and the compiler checks them and gives useful error messages.
Ultimately, something has to execute this state machine, and that something is a runtime.
Definition 17.12 (Executor). The part of a runtime responsible for executing the async code.
This is why main can’t be async (Listing 17.3): something else would need to manage the state machine for whatever future main returned, but main is the starting point of the program! Instead, we call trpl::block_on in main to set up a runtime and run the future returned by the async block until it’s done.
Remark. Some runtimes provide macros so you can write an async main. Those macros rewrite async fn main() { ... } to be a normal fn main, which does what we did by hand in Listing 17.4: call a function that runs a future to completion the way trpl::block_on does.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
27
Chapter 17 · 17.1 Futures and the Async Syntax
Racing two URLs against each other
Now we put the pieces together: call page_title with two different URLs passed in from the command line and race them by selecting whichever future finishes first.
Listing 17.5: Calling page_title for two URLs to see which returns first — src/main.rs
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::select(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
28
Chapter 17 · 17.1 Futures and the Async Syntax
Printing the winner; page_title returns its URL
Listing 17.5 (continued) — src/main.rs
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title was: '{title}'"),
None => println!("It had no title."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
page_title now returns a tuple: the URL it was given, plus the optional title.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
29
Chapter 17 · 17.1 Futures and the Async Syntax
trpl::select and the Either type
We call page_title for each user-supplied URL and save the resulting futures as title_fut_1 and title_fut_2. These don’t do anything yet: futures are lazy and we haven’t awaited them. Then we pass the futures to trpl::select, which returns a value to indicate which of the futures passed to it finishes first.
Remark. Under the hood, trpl::select is built on a more general select function defined in the futures crate. That function can do a lot of things trpl::select can’t, but it also has additional complexity we can skip over for now.
Either future can legitimately “win,” so it doesn’t make sense to return a Result. Instead, trpl::select returns trpl::Either. Like Result it has two cases, but there is no notion of success or failure baked in; it uses Left and Right to indicate “one or the other”:
enum Either<A, B> {
Left(A),
Right(B),
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
30
Chapter 17 · 17.1 Futures and the Async Syntax
Left, Right, and a working web scraper
The select function returns Left with that future’s output if the first argument wins, and Right with the second future argument’s output if that one wins. This matches the order the arguments appear in the call: the first argument is to the left of the second argument.
We also updated page_title to return the same URL passed in. That way, if the page that returns first does not have a <title> we can resolve, we can still print a meaningful message. With that information available, the println! output indicates both which URL finished first and what, if any, the <title> is for the web page at that URL.
You have built a small working web scraper! Pick a couple of URLs and run the command line tool. You may discover that some sites are consistently faster than others, while in other cases the faster site varies from run to run. More importantly, you’ve learned the basics of working with futures, so now we can dig deeper into what we can do with async.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
31
Chapter 17 · 17.2 Applying Concurrency with Async
Threads vs futures; creating a task with spawn_task
This section applies async to some of the same concurrency challenges we tackled with threads in Chapter 16. Because the key ideas were covered there, we focus on what’s different between threads and futures.
In many cases, the APIs for working with concurrency using async are very similar to those for using threads. In other cases, they end up being quite different. Even when the APIs look similar between threads and async, they often have different behavior — and they nearly always have different performance characteristics.
Creating a new task with spawn_task
The first operation in Chapter 16’s “Creating a New Thread with spawn” section was counting up on two separate threads. Let’s do the same using async. The trpl crate supplies a spawn_task function that looks very similar to the thread::spawn API, and a sleep function that is an async version of the thread::sleep API. Listing 17.6 uses them together to implement the counting example.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
32
Chapter 17 · 17.2 Applying Concurrency with Async
Counting on two tasks
Listing 17.6: Creating a new task to print one thing while the main task prints something else — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
33
Chapter 17 · 17.2 Applying Concurrency with Async
How Listing 17.6 runs
We set up main with trpl::block_on so that our top-level function can be async.
Remark. From here on, every example includes this exact same wrapping code with trpl::block_on in main, so the book often skips it, just as it does with main. Remember to include it in your code!
Then we write two loops within that block, each containing a trpl::sleep call that waits for half a second (500 milliseconds) before sending the next message. One loop is in the body of a trpl::spawn_task, the other in a top-level for loop. We add an await after the sleep calls.
This behaves similarly to the thread-based version — including that the messages may appear in a different order in your terminal:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
34
Chapter 17 · 17.2 Applying Concurrency with Async
Awaiting the task’s join handle
Listing 17.6 stops as soon as the for loop in the main async block finishes, because the task spawned by spawn_task is shut down when main ends. To run it all the way to completion, use a join handle to wait for the first task. With threads we used join to “block” until the thread was done; here we use await, because the task handle itself is a future. Its Output type is a Result, so we also unwrap it after awaiting it.
Listing 17.7: Using await with a join handle to run a task to completion — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let handle = trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
35
Chapter 17 · 17.2 Applying Concurrency with Async
handle.await instead of join
Listing 17.7 (continued) — src/main.rs
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
handle.await.unwrap();
});
}
This updated version runs until both loops finish (output on the next slide).
So far, async and threads give us similar outcomes, just with different syntax: using await instead of calling join on the join handle, and awaiting the sleep calls.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
36
Chapter 17 · 17.2 Applying Concurrency with Async
Both loops finish — without a new OS thread
Output of Listing 17.7:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
The bigger difference is that we didn’t need to spawn another operating system thread to do this.
In fact, we don’t even need to spawn a task here. Because async blocks compile to anonymous futures, we can put each loop in an async block and have the runtime run them both to completion using the trpl::join function.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
37
Chapter 17 · 17.2 Applying Concurrency with Async
trpl::join: awaiting two anonymous futures
Chapter 16 used the join method on the JoinHandle returned by std::thread::spawn. trpl::join is similar, but for futures: given two futures, it produces a single new future whose output is a tuple containing the output of each future once they both complete. We do not await fut1 and fut2, but the new future produced by trpl::join. We ignore its output: just a tuple of two unit values.
Listing 17.8: Using trpl::join to await two anonymous futures — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let fut1 = async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
38
Chapter 17 · 17.2 Applying Concurrency with Async
The second future, and joining them
Listing 17.8 (continued) — src/main.rs
let fut2 = async {
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
trpl::join(fut1, fut2).await;
});
}
Neither loop is spawned as a task: each is just an async block, and the one future we await is the one trpl::join returns. When we run this, both futures run to completion.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
39
Chapter 17 · 17.2 Applying Concurrency with Async
The same order every time: join is fair
Output of Listing 17.8:
hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
Now you’ll see the exact same order every time, which is very different from what we saw with threads and with trpl::spawn_task in Listing 17.7. That is because the trpl::join function is fair.
Definition 17.13 (Fair). A fair operation checks each future equally often, alternating between them, and never lets one race ahead if the other is ready.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
40
Chapter 17 · 17.2 Applying Concurrency with Async
Who decides what runs next
Table 17.2: Scheduling with threads versus async Rust
| Model | Who decides |
|---|---|
| Threads | The operating system decides which thread to check and how long to let it run. |
| Async Rust | The runtime decides which task to check. |
In practice, the details get complicated because an async runtime might use operating system threads under the hood as part of how it manages concurrency, so guaranteeing fairness can be more work for a runtime — but it’s still possible!
Runtimes don’t have to guarantee fairness for any given operation, and they often offer different APIs to let you choose whether or not you want fairness.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
41
Chapter 17 · 17.2 Applying Concurrency with Async
Try it: variations on awaiting the futures
Starting from Listing 17.8, try some of these variations and see what they do:
- Remove the async block from around either or both of the loops.
- Await each async block immediately after defining it.
- Wrap only the first loop in an async block, and await the resulting future after the body of the second loop.
For an extra challenge, see if you can figure out what the output will be in each case before running the code!
Remark. Each variation changes where the await points are and which futures are polled together — the two things that decide whether work interleaves.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
42
Chapter 17 · 17.2 Applying Concurrency with Async
Message passing between futures: an async channel
Sharing data between futures will also be familiar: message passing again, but with async versions of the types and functions. We take a slightly different path than Chapter 16’s “Transfer Data Between Threads with Message Passing” section, to show key differences between thread-based and futures-based concurrency. We begin with just a single async block — not spawning a separate task as we spawned a separate thread.
Listing 17.9: Creating an async channel and assigning the two halves to tx and rx — src/main.rs
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let val = String::from("hi");
tx.send(val).unwrap();
let received = rx.recv().await.unwrap();
println!("received '{received}'");
});
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
43
Chapter 17 · 17.2 Applying Concurrency with Async
How the async channel differs from the thread one
trpl::channel is an async version of the multiple-producer, single-consumer channel API used with threads in Chapter 16. It is only a little different:
- it uses a mutable rather than an immutable receiver rx, and its recv method produces a future we need to await rather than producing the value directly;
- we don’t have to spawn a separate thread or even a task; we merely need to await the rx.recv call.
The synchronous Receiver::recv in std::mpsc::channel blocks until it receives a message. trpl::Receiver::recv does not, because it is async: it hands control back to the runtime until either a message is received or the send side of the channel closes. By contrast, we don’t await the send call, because it doesn’t block — it doesn’t need to, since the channel we’re sending into is unbounded.
Remark. Because all of this async code runs in an async block in a trpl::block_on call, everything within it can avoid blocking. However, the code outside it will block on the block_on function returning. That’s the whole point of trpl::block_on: it lets you choose where to block on some set of async code, and thus where to transition between sync and async code.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
44
Chapter 17 · 17.2 Applying Concurrency with Async
Sending a series of messages
Notice two things about Listing 17.9. First, the message arrives right away. Second, although we use a future, there’s no concurrency yet: everything happens in sequence, just as it would with no futures involved. To address the first, we send a series of messages and sleep in between them.
Listing 17.10: Sending and receiving multiple messages over the async channel and sleeping with an await between each message — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
45
Chapter 17 · 17.2 Applying Concurrency with Async
Sending with sleeps, receiving in a loop
Listing 17.10 (continued) — src/main.rs
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
}
Besides sending the messages, we need to receive them. Because we know how many are coming, we could call rx.recv().await four times. In the real world, though, we’ll generally be waiting on some unknown number of messages, so we need to keep waiting until we determine that there are no more messages.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
46
Chapter 17 · 17.2 Applying Concurrency with Async
Receiving an unknown number of messages: while let
In Listing 16-10 we used a for loop to process all the items received from a synchronous channel. Rust doesn’t yet have a way to use a for loop with an asynchronously produced series of items, so we use the while let conditional loop — the loop version of the if let construct from Chapter 6. The loop continues executing as long as the pattern it specifies continues to match the value.
The rx.recv call produces a future, which we await. The runtime pauses the future until it is ready. Once a message arrives, the future resolves to Some(message), as many times as a message arrives. When the channel closes, regardless of whether any messages have arrived, the future instead resolves to None: there are no more values, so we should stop polling — that is, stop awaiting.
The while let loop pulls this together. If rx.recv().await gives Some(message), we can use the message in the loop body, just as with if let. If the result is None, the loop ends. Every time the loop completes, it hits the await point again, so the runtime pauses it again until another message arrives.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
47
Chapter 17 · 17.2 Applying Concurrency with Async
Code within one async block executes linearly
Listing 17.10 sends and receives all of the messages, but two problems remain:
- the messages do not arrive at half-second intervals: they arrive all at once, 2 seconds (2,000 milliseconds) after we start the program;
- the program never exits: it waits forever for new messages, and you have to shut it down using ctrl-C.
Within one async block, await keywords execute in the order they appear in the code. Listing 17.10 has one async block, so everything runs linearly — still no concurrency: all the tx.send calls happen, interspersed with the trpl::sleep await points, and only then does the while let loop reach any recv await point.
To get a sleep delay between each message, we put the tx and rx operations in their own async blocks, so the runtime can execute each separately using trpl::join, as in Listing 17.8. Again we await the result of trpl::join, not the individual futures: awaiting them in sequence would put us back in a sequential flow — exactly what we’re trying not to do.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
48
Chapter 17 · 17.2 Applying Concurrency with Async
Separate async blocks for sending and receiving
Listing 17.11: Separating send and recv into their own async blocks and awaiting the futures for those blocks — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
49
Chapter 17 · 17.2 Applying Concurrency with Async
Joining the sender and receiver futures
Listing 17.11 (continued) — src/main.rs
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
Now the messages print at 500-millisecond intervals, rather than all in a rush after 2 seconds.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
50
Chapter 17 · 17.2 Applying Concurrency with Async
Why the program still never exits
The program never exits because of the way the while let loop interacts with trpl::join:
- The future returned from trpl::join completes only once both futures passed to it have completed.
- The tx_fut future completes once it finishes sleeping after sending the last message in vals.
- The rx_fut future won’t complete until the while let loop ends.
- The while let loop won’t end until awaiting rx.recv produces None.
- Awaiting rx.recv will return None only once the other end of the channel is closed.
- The channel will close only if we call rx.close or when the sender side, tx, is dropped.
- We don’t call rx.close anywhere, and tx won’t be dropped until the outermost async block passed to trpl::block_on ends.
- The block can’t end because it is blocked on trpl::join completing, which takes us back to the top of this list.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
51
Chapter 17 · 17.2 Applying Concurrency with Async
Moving ownership into an async block
Right now, the async block where we send the messages only borrows tx, because sending a message doesn’t require ownership. But if we could move tx into that async block, it would be dropped once that block ends — and the channel would close.
Chapter 13’s “Capturing References or Moving Ownership” section showed the move keyword with closures, and Chapter 16’s “Using move Closures with Threads” showed that we often need to move data into closures when working with threads. The same basic dynamics apply to async blocks, so the move keyword works with async blocks just as it does with closures.
In Listing 17.12, we change the block used to send messages from async to async move.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
52
Chapter 17 · 17.2 Applying Concurrency with Async
async move: the program now shuts down
Listing 17.12: A revision of the code from Listing 17.11 that correctly shuts down when complete — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async move {
// --snip--
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
The only change from Listing 17.11 is async move on tx_fut (its body, elided here, is unchanged). tx is now owned by that block, so it is dropped when the block finishes; the channel closes, rx.recv yields None, and the loop ends.
When we run this version, it shuts down gracefully after the last message is sent and received. Next: sending data from more than one future.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
53
Chapter 17 · 17.2 Applying Concurrency with Async
Multiple producers: cloning tx
This async channel is also a multiple-producer channel, so we can call clone on tx if we want to send messages from multiple futures.
Listing 17.13: Using multiple producers with async blocks — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
54
Chapter 17 · 17.2 Applying Concurrency with Async
The first sender, the receiver, a second sender
Listing 17.13 (continued) — src/main.rs
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
55
Chapter 17 · 17.2 Applying Concurrency with Async
A slower second sender, and join!
Listing 17.13 (continued) — src/main.rs
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(1500)).await;
}
};
trpl::join!(tx1_fut, tx_fut, rx_fut);
});
}
Three futures now: tx1_fut (every 500 ms), tx_fut (every 1,500 ms) and rx_fut, all awaited together by trpl::join!.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
56
Chapter 17 · 17.2 Applying Concurrency with Async
Joining a number of futures with the join! macro
First, we clone tx, creating tx1 outside the first async block, and move tx1 into that block just as we did before with tx. Later, we move the original tx into a new async block, where we send more messages on a slightly slower delay. We happen to put this new block after the receiving block, but it could go before it just as well. The key is the order in which the futures are awaited, not the order in which they’re created.
Both sending blocks need to be async move blocks, so that both tx and tx1 get dropped when those blocks finish. Otherwise, we’ll end up back in the same infinite loop we started out in.
Finally, we switch from trpl::join to trpl::join! to handle the additional future: the join! macro awaits an arbitrary number of futures where we know the number of futures at compile time. Awaiting a collection of an unknown number of futures comes later in this chapter.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
57
Chapter 17 · 17.2 Applying Concurrency with Async
Messages from both senders, at their own intervals
Now we see all the messages from both sending futures, and because the sending futures use slightly different delays after sending, the messages are also received at those different intervals:
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
We’ve explored how to use message passing to send data between futures, how code within an async block runs sequentially, how to move ownership into an async block, and how to join multiple futures. Next: how and why to tell the runtime it can switch to another task.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
58
Chapter 17 · 17.3 Working with Any Number of Futures
Yielding control to the runtime
At each await point, Rust gives a runtime a chance to pause the task and switch to another one if the future being awaited isn’t ready. The inverse is also true: Rust only pauses async blocks and hands control back to a runtime at an await point. Everything between await points is synchronous.
So if you do a bunch of work in an async block without an await point, that future will block any other futures from making progress.
Definition 17.14 (Starvation). A future starves other futures when it does work without reaching an await point, keeping them from making progress.
In some cases, that may not be a big deal. However, if you are doing some kind of expensive setup or long-running work, or if you have a future that will keep doing some particular task indefinitely, you’ll need to think about when and where to hand control back to the runtime.
Let’s simulate a long-running operation to illustrate the starvation problem, then explore how to solve it.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
59
Chapter 17 · 17.3 Working with Any Number of Futures
A slow, blocking operation
Listing 17.14: Using thread::sleep to simulate slow operations — src/main.rs
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
// We will call `slow` here later
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
This code uses std::thread::sleep instead of trpl::sleep, so calling slow blocks the current thread for some number of milliseconds. We can use slow to stand in for real-world operations that are both long-running and blocking.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
60
Chapter 17 · 17.3 Working with Any Number of Futures
Two futures doing CPU-bound work
We use slow to emulate CPU-bound work in a pair of futures, raced with trpl::select.
Listing 17.15: Calling the slow function to simulate slow operations — src/main.rs
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
slow("a", 10);
slow("a", 20);
trpl::sleep(Duration::from_millis(50)).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
61
Chapter 17 · 17.3 Working with Any Number of Futures
Racing the two slow futures
Listing 17.15 (continued) — src/main.rs
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
62
Chapter 17 · 17.3 Working with Any Number of Futures
No interleaving without await points
Output of Listing 17.15:
'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
Each future hands control back to the runtime only after carrying out a bunch of slow operations.
As in Listing 17.5, select still finishes as soon as a is done. But there’s no interleaving between the calls to slow: a does all its work until its trpl::sleep is awaited, then b does all its work until its own trpl::sleep is awaited, and finally a completes.
To let both futures make progress between their slow tasks, we need await points to hand control back to the runtime — something we can await! The handoff is already visible: without the trpl::sleep at the end of a, it would complete without b running at all. Let’s use trpl::sleep as a starting point for letting operations switch off making progress.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
63
Chapter 17 · 17.3 Working with Any Number of Futures
Adding await points with trpl::sleep
Listing 17.16: Using trpl::sleep to let operations switch off making progress — src/main.rs
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let one_ms = Duration::from_millis(1);
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 350);
trpl::sleep(one_ms).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
64
Chapter 17 · 17.3 Working with Any Number of Futures
Now the two futures’ work is interleaved
Listing 17.16 (continued) — src/main.rs
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
We’ve added trpl::sleep calls with await points between each call to slow. Now the two futures’ work is interleaved:
Output of Listing 17.16:
'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.
The a future still runs for a bit before handing off control to b, because it calls slow before ever calling trpl::sleep; after that the futures swap back and forth each time one of them hits an await point. Here we did that after every call to slow, but we could break up the work in whatever way makes the most sense to us.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
65
Chapter 17 · 17.3 Working with Any Number of Futures
Handing back control without sleeping: yield_now
We don’t really want to sleep here, though: we want to make progress as fast as we can. We just need to hand back control to the runtime. We can do that directly, using the trpl::yield_now function. In Listing 17.17, we replace all those trpl::sleep calls with trpl::yield_now.
Table 17.3: Two ways to create an await point between slow calls
| Call | What it does |
|---|---|
| trpl::sleep(one_ms).await | Waits on a timer before resuming; this sleep always waits at least a millisecond. |
| trpl::yield_now().await | Simply hands control back to the runtime, with no timer involved. |
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
66
Chapter 17 · 17.3 Working with Any Number of Futures
Yielding with trpl::yield_now
Listing 17.17: Using yield_now to let operations switch off making progress — src/main.rs
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 350);
trpl::yield_now().await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
67
Chapter 17 · 17.3 Working with Any Number of Futures
Why yield_now beats sleep here
Listing 17.17 (continued) — src/main.rs
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
This code is both clearer about the actual intent and can be significantly faster than using sleep, because timers such as the one used by sleep often have limits on how granular they can be. The version of sleep we are using, for example, will always sleep for at least a millisecond, even if we pass it a Duration of one nanosecond. Again, modern computers are fast: they can do a lot in one millisecond!
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
68
Chapter 17 · 17.3 Working with Any Number of Futures
Cooperative multitasking
Async can be useful even for compute-bound tasks, depending on what else your program is doing, because it provides a useful tool for structuring the relationships between different parts of the program — at the cost of the overhead of the async state machine.
Definition 17.15 (Cooperative multitasking). A form of multitasking in which each future has the power to determine when it hands over control, via await points — and therefore also the responsibility to avoid blocking for too long.
In some Rust-based embedded operating systems, this is the only kind of multitasking!
In real-world code, you won’t usually alternate function calls with await points on every single line. Yielding control this way is relatively inexpensive, but it’s not free. Trying to break up a compute-bound task might make it significantly slower, so sometimes it’s better for overall performance to let an operation block briefly. Always measure to see what your code’s actual performance bottlenecks are. Keep the underlying dynamic in mind, though, if you are seeing a lot of work happening in serial that you expected to happen concurrently!
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
69
Chapter 17 · 17.3 Working with Any Number of Futures
Building our own async abstraction: timeout
We can also compose futures together to create new patterns. For example, we can build a timeout function with async building blocks we already have. The result will be another building block we could use to create still more async abstractions. Listing 17.18 (next slide) shows how we would expect this timeout to work with a slow future.
To implement it, first think about the API for timeout:
- It needs to be an async function itself so we can await it.
- Its first parameter should be a future to run. We can make it generic to allow it to work with any future.
- Its second parameter will be the maximum time to wait. If we use a Duration, that will make it easy to pass along to trpl::sleep.
- It should return a Result. If the future completes successfully, the Result will be Ok with the value produced by the future. If the timeout elapses first, the Result will be Err with the duration that the timeout waited for.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
70
Chapter 17 · 17.3 Working with Any Number of Futures
How timeout should be used
Listing 17.18: Using our imagined timeout to run a slow operation with a time limit — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
71
Chapter 17 · 17.3 Working with Any Number of Futures
The signature of timeout
Listing 17.19 shows this declaration (main stays as in Listing 17.18):
Listing 17.19: Defining the signature of timeout — src/main.rs
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
That satisfies our goals for the types. Now the behavior: we want to race the future passed in against the duration. We can use trpl::sleep to make a timer future from the duration, and use trpl::select to run that timer with the future the caller passes in.
In Listing 17.20, we implement timeout by matching on the result of awaiting trpl::select.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
72
Chapter 17 · 17.3 Working with Any Number of Futures
Implementing timeout with select and sleep
Listing 17.20: Defining timeout with select and sleep — src/main.rs
use std::time::Duration;
use trpl::Either;
// --snip--
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
match trpl::select(future_to_try, trpl::sleep(max_time)).await {
Either::Left(output) => Ok(output),
Either::Right(_) => Err(max_time),
}
}
The --snip-- stands for the unchanged main from Listing 17.18.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
73
Chapter 17 · 17.3 Working with Any Number of Futures
Why argument order matters; the result
The implementation of trpl::select is not fair: it always polls arguments in the order in which they are passed (other select implementations randomly choose which argument to poll first). So we pass future_to_try to select first, so it gets a chance to complete even if max_time is a very short duration. If future_to_try finishes first, select returns Left with its output; if the timer finishes first, select returns Right with the timer’s output of ().
If future_to_try succeeds and we get a Left(output), we return Ok(output). If the sleep timer elapses instead and we get a Right(()), we ignore the () with _ and return Err(max_time).
With that, we have a working timeout built out of two other async helpers. Running the code prints the failure mode after the timeout:
Failed after 2 seconds
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
74
Chapter 17 · 17.3 Working with Any Number of Futures
Futures compose
Because futures compose with other futures, you can build really powerful tools using smaller async building blocks. For example, you can use this same approach to combine timeouts with retries, and in turn use those with operations such as network calls (like those in Listing 17.5).
In practice, you’ll usually work directly with async and await, and secondarily with functions such as select and macros such as join! to control how the outermost futures are executed.
Table 17.4: Building blocks for combining futures used so far
| Building block | Completes when |
|---|---|
| trpl::join(a, b) | both futures are done; output is a tuple of both outputs |
| trpl::join!(a, b, c, …) | all futures are done; their number is known at compile time |
| trpl::select(a, b) | the first future finishes; output is Either::Left or Either::Right |
Next: working with multiple futures in a sequence over time, with streams.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
75
Chapter 17 · 17.4 Streams: Futures in Sequence
Streams: a sequence of items over time
Recall the receiver for our async channel in Section 17.2: the async recv method produces a sequence of items over time. This is an instance of a much more general pattern known as a stream.
Definition 17.16 (Stream). A sequence of items that become available over time — like an asynchronous form of iteration.
Many concepts are naturally represented as streams:
- items becoming available in a queue;
- chunks of data pulled incrementally from the filesystem when the full data set is too large for the computer’s memory;
- data arriving over the network over time.
Because streams are futures, we can use them with any other kind of future and combine them in interesting ways: batch up events to avoid triggering too many network calls, set timeouts on sequences of long-running operations, or throttle user interface events to avoid doing needless work.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
76
Chapter 17 · 17.4 Streams: Futures in Sequence
Iterators and streams
Chapter 13’s Iterator trait also gave us a sequence of items, but there are two differences between iterators and the async channel receiver:
Table 17.5: Iterator versus the async channel receiver
| Difference | Iterator | trpl::Receiver |
|---|---|---|
| Time | synchronous | asynchronous |
| API | call its synchronous next method | call its asynchronous recv method |
Otherwise, these APIs feel very similar, and that similarity isn’t a coincidence: a stream is like an asynchronous form of iteration. Whereas trpl::Receiver specifically waits to receive messages, the general-purpose stream API is much broader: it provides the next item the way Iterator does, but asynchronously.
Because of this similarity, we can create a stream from any iterator. As with an iterator, we work with a stream by calling its next method and then awaiting the output, as in Listing 17.21 — which won’t compile yet.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
77
Chapter 17 · 17.4 Streams: Futures in Sequence
A stream from an iterator
Listing 17.21: Creating a stream from an iterator and printing its values — src/main.rs
fn main() {
trpl::block_on(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
We start with an array of numbers, which we convert to an iterator and then call map on to double all the values. Then we convert the iterator into a stream using the trpl::stream_from_iter function. Next, we loop over the items in the stream as they arrive with the while let loop.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
78
Chapter 17 · 17.4 Streams: Futures in Sequence
Error: no method named next
Unfortunately, when we try to run the code, it doesn’t compile, but instead reports that there’s no next method available:
error[E0599]: no method named `next` found for struct `tokio_stream::iter::Iter` in the current scope
--> src/main.rs:10:40
|
10 | while let Some(value) = stream.next().await {
| ^^^^
|
= help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope;
perhaps you want to import one of them
|
1 + use crate::trpl::StreamExt;
|
1 + use futures_util::stream::stream::StreamExt;
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
79
Chapter 17 · 17.4 Streams: Futures in Sequence
The missing trait is StreamExt
|
1 + use std::iter::Iterator;
|
1 + use std::str::pattern::Searcher;
|
help: there is a method `try_next` with a similar name
|
10 | while let Some(value) = stream.try_next().await {
| ~~~~~~~~
As this output explains, we need the right trait in scope to be able to use the next method. Given our discussion so far, you might reasonably expect that trait to be Stream, but it’s actually StreamExt.
Definition 17.17 (Extension trait, Ext). Ext, short for extension, is a common pattern in the Rust community for extending one trait with another.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
80
Chapter 17 · 17.4 Streams: Futures in Sequence
Stream, StreamExt, and the fix
The Stream trait defines a low-level interface that effectively combines the Iterator and Future traits. StreamExt supplies a higher-level set of APIs on top of Stream, including next and other utility methods similar to those of Iterator. Stream and StreamExt are not yet part of the standard library, but most ecosystem crates use similar definitions.
The fix: add a use statement for trpl::StreamExt.
Listing 17.22: Successfully using an iterator as the basis for a stream — src/main.rs
use trpl::StreamExt;
fn main() {
trpl::block_on(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
Now the code works the way we want! And with StreamExt in scope, we can use all of its utility methods, just as with iterators.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
81
Chapter 17 · 17.5 A Closer Look at the Traits for Async
The Future trait
We’ve used the Future, Stream, and StreamExt traits without details of how they work or fit together, which is fine most of the time. Sometimes, though, you’ll need more of their details, along with the Pin type and the Unpin trait. This section digs in just enough for those scenarios, leaving the really deep dive to other documentation.
Here’s how Rust defines the Future trait:
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
That definition includes a bunch of new types and some syntax we haven’t seen before, so let’s walk through it piece by piece.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
82
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Output, poll, and the Poll type
First, Future’s associated type Output says what the future resolves to — analogous to the Item associated type of the Iterator trait. Second, Future has the poll method, which takes a special Pin reference for its self parameter and a mutable reference to a Context type, and returns a Poll<Self::Output>. (Pin and Context come in a moment.) For now, focus on the return type, Poll:
pub enum Poll<T> {
Ready(T),
Pending,
}
Poll is similar to an Option: one variant has a value, Ready(T), and one does not, Pending. But Poll means something quite different from Option!
- Pending: the future still has work to do, so the caller will need to check again later.
- Ready: the Future has finished its work and the T value is available.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
83
Chapter 17 · 17.5 A Closer Look at the Traits for Async
await compiles to calls to poll
Remark. It’s rare to need to call poll directly, but if you do, keep in mind that with most futures, the caller should not call poll again after the future has returned Ready. Many futures will panic if polled again after becoming ready. Futures that are safe to poll again will say so explicitly in their documentation. This is similar to how Iterator::next behaves.
When you see code that uses await, Rust compiles it under the hood to code that calls poll. Listing 17.4, where we printed the page title for a single URL once it resolved, compiles into something kind of (although not exactly) like this:
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
84
Chapter 17 · 17.5 A Closer Look at the Traits for Async
What to do while Pending: a loop?
What should we do when the future is still Pending? We need some way to try again, and again, and again, until the future is finally ready. In other words, we need a loop:
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
If Rust compiled await to exactly that code, though, every await would be blocking — exactly the opposite of what we were going for!
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
85
Chapter 17 · 17.5 A Closer Look at the Traits for Async
The runtime polls futures and puts them to sleep
Instead of a blocking loop, Rust ensures that the loop can hand off control to something that can pause work on this future to work on other futures and then check this one again later. As we’ve seen, that something is an async runtime, and this scheduling and coordination work is one of its main jobs.
In Section 17.2 we described waiting on rx.recv: the recv call returns a future, and awaiting the future polls it. A runtime pauses the future until it’s ready with either Some(message) or None when the channel closes. Now we can see how that works:
- the runtime knows the future isn’t ready when poll returns Poll::Pending;
- the runtime knows the future is ready, and advances it, when poll returns Poll::Ready(Some(message)) or Poll::Ready(None).
The exact details of how a runtime does that are beyond the scope of this book, but the key is the basic mechanics of futures: a runtime polls each future it is responsible for, putting the future back to sleep when it is not yet ready.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
86
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Pin and Unpin: futures in a collection
In Listing 17.13 we used trpl::join! to await three futures. However, it’s common to have a collection such as a vector containing some number of futures that won’t be known until runtime. Listing 17.23 puts the three futures into a vector and calls trpl::join_all instead — which won’t compile yet.
Listing 17.23: Awaiting futures in a collection — src/main.rs
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
87
Chapter 17 · 17.5 A Closer Look at the Traits for Async
The same three futures as before
Listing 17.23 (continued) — src/main.rs
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
88
Chapter 17 · 17.5 A Closer Look at the Traits for Async
A vector of boxed futures, passed to join_all
Listing 17.23 (continued) — src/main.rs
let tx_fut = async move {
// --snip--
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
The --snip-- stands for the same tx_fut body as in Listing 17.13 (with 1-second sleeps). We put each future within a Box to make them into trait objects, just as in Chapter 12’s “Returning Errors from run” section (trait objects are covered in detail in Chapter 18). Trait objects let us treat each of the anonymous futures produced by these types as the same type, because all of them implement the Future trait.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
89
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Why trait objects, and why it fails to compile
This might be surprising: none of the async blocks returns anything, so each one produces a Future<Output = ()>. But Future is a trait, and the compiler creates a unique enum for each async block, even when they have identical output types. Just as you can’t put two different handwritten structs in a Vec, you can’t mix compiler-generated enums.
Then we pass the collection of futures to trpl::join_all and await the result. However, this doesn’t compile; here’s the relevant part of the error messages:
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
90
Chapter 17 · 17.5 A Closer Look at the Traits for Async
“cannot be unpinned”: the rest of the error
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/
futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
The note tells us to use the pin! macro to pin the values, which means putting them inside the Pin type that guarantees the values won’t be moved in memory. Pinning is required because dyn Future<Output = ()> needs to implement the Unpin trait, and it currently does not.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
91
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Why JoinAll needs Unpin
trpl::join_all returns a struct called JoinAll, generic over a type F that is constrained to implement the Future trait. Directly awaiting a future with await pins the future implicitly. That’s why we don’t need to use pin! everywhere we want to await futures.
However, we’re not directly awaiting a future here. Instead, we construct a new future, JoinAll, by passing a collection of futures to join_all. The signature for join_all requires that the types of the items in the collection all implement the Future trait, and Box<T> implements Future only if the T it wraps is a future that implements the Unpin trait.
Example 17.18. The chain of requirements behind the error:
- JoinAll<F> needs every item F to implement Future;
- each item is a Box<dyn Future<Output = ()>>, which implements Future only if dyn Future<Output = ()> implements Unpin;
- it doesn’t — so the code is rejected.
That’s a lot to absorb! Let’s dive further into how Future works, in particular around pinning.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
92
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Back to the Future trait: cx and self
Look again at the definition of the Future trait:
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
The cx parameter and its Context type are the key to how a runtime actually knows when to check any given future while still being lazy. The details are beyond the scope of this chapter, and you generally only need to think about this when writing a custom Future implementation. We focus instead on the type for self: this is the first time we’ve seen a method where self has a type annotation.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
93
Chapter 17 · 17.5 A Closer Look at the Traits for Async
A type annotation on self, and the Pin wrapper
A type annotation for self works like type annotations for other function parameters, with two key differences:
- it tells Rust what type self must be for the method to be called;
- it can’t be just any type: it’s restricted to the type on which the method is implemented, a reference or smart pointer to that type, or a Pin wrapping a reference to that type.
More on this syntax in Chapter 18. For now: to poll a future to check whether it is Pending or Ready(Output), we need a Pin-wrapped mutable reference to the type.
Definition 17.19 (Pin). A wrapper for pointer-like types such as &, &mut, Box, and Rc. (Technically, Pin works with types that implement Deref or DerefMut, which is effectively equivalent to working only with references and smart pointers.)
Pin is not a pointer itself and doesn’t have any behavior of its own, like Rc and Arc do with reference counting; it’s purely a tool the compiler can use to enforce constraints on pointer usage.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
94
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Why poll needs a pinned self
Recalling that await is implemented in terms of calls to poll starts to explain the error message we saw earlier — but that was in terms of Unpin, not Pin. So how exactly does Pin relate to Unpin, and why does Future need self to be in a Pin type to call poll?
A series of await points in a future gets compiled into a state machine, and the compiler makes sure that state machine follows all of Rust’s normal rules around safety, including borrowing and ownership. To make that work, Rust looks at what data is needed between one await point and either the next await point or the end of the async block. It then creates a corresponding variant in the compiled state machine. Each variant gets the access it needs to the data used in that section of the source code, whether by taking ownership of that data or by getting a mutable or immutable reference to it.
So far, so good: if we get anything wrong about the ownership or references in a given async block, the borrow checker will tell us. When we want to move around the future that corresponds to that block — like moving it into a Vec to pass to join_all — things get trickier.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
95
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Futures can reference themselves
When we move a future — whether by pushing it into a data structure to use as an iterator with join_all or by returning it from a function — that actually means moving the state machine Rust creates for us.
And unlike most other types in Rust, the futures Rust creates for async blocks can end up with references to themselves in the fields of any given variant, as in the simplified illustration of Figure 17.4.
Definition 17.20 (Self-referential type). A data type that holds a reference to (part of) itself in one of its own fields.
Figure 17.4: A self-referential data type
fut1
0
1
↑ reference to row 2
The third field points back into the same future: an internal reference.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
96
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Moving a self-referential value is unsafe
By default, any object that has a reference to itself is unsafe to move, because references always point to the actual memory address of whatever they refer to. If you move the data structure itself, those internal references are left pointing to the old location — which is now invalid. For one thing, its value will not be updated when you change the data structure. For another, more important, thing, the computer is now free to reuse that memory for other purposes! You could end up reading completely unrelated data later.
Figure 17.5: The unsafe result of moving a self-referential data type
fut1 (moved out of)
?
? (old row 2)
?
fut2 (new location)
0
1
← still points to fut1, row 2
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
97
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Pinning: making sure the data doesn’t move
Theoretically, the compiler could try to update every reference to an object whenever it gets moved, but that could add a lot of performance overhead, especially if a whole web of references needs updating. If we could instead make sure the data structure doesn’t move in memory, we wouldn’t have to update any references. This is exactly what the borrow checker is for: in safe code, it prevents you from moving any item with an active reference to it.
Pin builds on that to give us the exact guarantee we need. When we pin a value by wrapping a pointer to that value in Pin, it can no longer move. Thus, with Pin<Box<SomeType>>, you actually pin the SomeType value, not the Box pointer.
Figure 17.6: Pinning a Box that points to a self-referential future type
Pin
→
b1
→
pinned: fut
0
• → last cell
…
1
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
98
Chapter 17 · 17.5 A Closer Look at the Traits for Async
The pointer may move; the pinned data may not
In fact, the Box pointer can still move around freely. Remember: we care about making sure the data ultimately being referenced stays in place. If a pointer moves around, but the data it points to is in the same place, there’s no potential problem. The key is that the self-referential type itself cannot move, because it is still pinned.
(As an independent exercise, look at the docs for the types as well as the std::pin module and try to work out how you’d do this with a Pin wrapping a Box.)
Figure 17.7: Moving a Box which points to a self-referential future type
Pin
→
b1 (old)
b2
→
pinned: fut
0
• → last cell
…
1
The pointer moved from b1 to b2, but the pinned future stayed where it was.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
99
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Most types are safe to move: Unpin
However, most types are perfectly safe to move around, even if they happen to be behind a Pin pointer. We only need to think about pinning when items have internal references. Primitive values such as numbers and Booleans obviously don’t have any internal references; neither do most types you normally work with in Rust. You can move around a Vec, for example, without worrying.
Yet with a Pin<Vec<String>>, you’d have to do everything via the safe but restrictive APIs provided by Pin, even though a Vec<String> is always safe to move if there are no other references to it. We need a way to tell the compiler that it’s fine to move items around in cases like this — and that’s where Unpin comes into play.
Definition 17.21 (Unpin). A marker trait informing the compiler that a given type does not need to uphold any guarantees about whether the value in question can be safely moved.
Like Send and Sync from Chapter 16, Unpin has no functionality of its own. Marker traits exist only to tell the compiler it’s safe to use the type implementing a given trait in a particular context.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
100
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Unpin is the normal case; !Unpin the special one
Just as with Send and Sync, the compiler implements Unpin automatically for all types where it can prove it is safe. The special case is where Unpin is not implemented for a type, written impl !Unpin for SomeType, where SomeType is a type that does need to uphold those guarantees to be safe whenever a pointer to it is used in a Pin.
Two things to keep in mind about Pin and Unpin: first, Unpin is the “normal” case, and !Unpin is the special case. Second, whether a type implements Unpin or !Unpin only matters when you’re using a pinned pointer to that type, like Pin<&mut SomeType>.
Concretely, a String has a length and the Unicode characters that make it up. We can wrap a String in Pin, but String automatically implements Unpin, as do most other types in Rust.
Figure 17.8: Pinning a String; the dotted line indicates that the String implements the Unpin trait and thus is not pinned
Pin
→
String
5usize
h e l l o
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
101
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Replacing a pinned String is fine
As a result, we can do things that would be illegal if String implemented !Unpin instead, such as replacing one string with another at the exact same location in memory. This doesn’t violate the Pin contract, because String has no internal references that make it unsafe to move around. That is precisely why it implements Unpin rather than !Unpin.
Figure 17.9: Replacing the String with an entirely different String in memory
Pin
→
s1 (replaced)
5usize
h e l l o
s2
7usize
g o o d b y e
Now we can understand the errors for join_all in Listing 17.23. We tried to move the futures produced by async blocks into a Vec<Box<dyn Future<Output = ()>>>, but those futures may have internal references, so they don’t automatically implement Unpin. Once we pin them, we can pass the resulting Pin type into the Vec, confident that the underlying data in the futures will not be moved.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
102
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Fixing join_all with pin!
Listing 17.24 fixes the code by calling the pin! macro where each of the three futures is defined, and adjusting the trait object type.
Listing 17.24: Pinning the futures to enable moving them into the vector
use std::pin::{Pin, pin};
// --snip--
let tx1_fut = pin!(async move {
// --snip--
});
let rx_fut = pin!(async {
// --snip--
});
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
103
Chapter 17 · 17.5 A Closer Look at the Traits for Async
A Vec of pinned mutable references
Listing 17.24 (continued)
let tx_fut = pin!(async move {
// --snip--
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
This example now compiles and runs, and we could add or remove futures from the vector at runtime and join them all.
Pin and Unpin are mostly important for building lower-level libraries, or when you’re building a runtime itself, rather than for day-to-day Rust code. When you see these traits in error messages, though, now you’ll have a better idea of how to fix your code!
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
104
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Beyond async; where to learn more about pinning
Remark. This combination of Pin and Unpin makes it possible to safely implement a whole class of complex types in Rust that would otherwise prove challenging because they’re self-referential. Types that require Pin show up most commonly in async Rust today, but every once in a while, you might see them in other contexts, too.
Table 17.6: Further reading on pinning
| Source | What it covers |
|---|---|
| API documentation for std::pin | The specifics of how Pin and Unpin work, and the rules they’re required to uphold, covered extensively; a great place to start. |
| Asynchronous Programming in Rust (rust-lang.github.io/async-book), Chapters 2 and 4 | How things work under the hood in even more detail: execution (Chapter 2) and pinning (Chapter 4). |
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
105
Chapter 17 · 17.5 A Closer Look at the Traits for Async
The Stream trait: Iterator meets Future
Streams are similar to asynchronous iterators. Unlike Iterator and Future, however, Stream has no definition in the standard library as of this writing, but there is a very common definition from the futures crate used throughout the ecosystem.
From Iterator, we have the idea of a sequence: its next method provides an Option<Self::Item>. From Future, we have the idea of readiness over time: its poll method provides a Poll<Self::Output>. To represent a sequence of items that become ready over time, a Stream trait puts those features together.
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
106
Chapter 17 · 17.5 A Closer Look at the Traits for Async
Item and poll_next
The associated type Item is the type of the items produced: zero to many, as with Iterator, unlike Future’s always-single Output (even if it’s the unit type ()).
The method is called poll_next to make clear that it polls in the same way Future::poll does and produces a sequence of items in the same way Iterator::next does. Its return type combines Poll with Option: the outer type is Poll, because it has to be checked for readiness, just as a future does; the inner type is Option, because it needs to signal whether there are more messages, just as an iterator does.
Table 17.7: Iterator, Future and Stream side by side
| Trait | Method | Returns | Items |
|---|---|---|---|
| Iterator | next | Option<Self::Item> | zero to many, synchronously |
| Future | poll | Poll<Self::Output> | exactly one Output |
| Stream | poll_next | Poll<Option<Self::Item>> | zero to many, over time |
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
107
Chapter 17 · 17.5 A Closer Look at the Traits for Async
StreamExt supplies next
Something very similar to Stream will likely end up in Rust’s standard library. Meanwhile, it’s part of the toolkit of most runtimes, so you can rely on it.
In Section 17.4 we didn’t use poll_next or Stream, but next and StreamExt.
We could work directly in terms of the poll_next API by hand-writing our own Stream state machines, just as we could work with futures directly via their poll method. Using await is much nicer, though, and the StreamExt trait supplies the next method so we can do just that.
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>>;
}
trait StreamExt: Stream {
async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;
// other methods...
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
108
Chapter 17 · 17.5 A Closer Look at the Traits for Async
The real next, and why the traits are separate
Remark. The actual definition we used earlier in the chapter looks slightly different, because it supports versions of Rust that did not yet support async functions in traits:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;
That Next type is a struct that implements Future and lets us name the lifetime of the reference to self with Next<'_, Self>, so that await can work with this method.
StreamExt is also the home of all the interesting methods available to use with streams. It is automatically implemented for every type that implements Stream, but the traits are defined separately so the community can iterate on convenience APIs without affecting the foundational trait.
In the trpl version of StreamExt, the trait not only defines next but also supplies a default implementation that correctly handles the details of calling Stream::poll_next. So even when you write your own streaming data type, you only have to implement Stream; anyone who uses your type can use StreamExt and its methods with it automatically.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
109
Chapter 17 · 17.6 Putting It All Together: Futures, Tasks, and Threads
Threads or async? Often both
Chapter 16 showed threads as one approach to concurrency; this chapter showed another, async with futures and streams. When to choose one over the other? It depends! And in many cases, the choice isn’t threads or async but rather threads and async.
Many operating systems have supplied threading-based concurrency models for decades, and many programming languages support them as a result. But they have tradeoffs: on many operating systems, they use a fair bit of memory for each thread. Threads are also only an option when your OS and hardware support them. Unlike mainstream desktop and mobile computers, some embedded systems don’t have an OS at all, so they also don’t have threads.
The async model provides a different — and ultimately complementary — set of tradeoffs. In the async model, concurrent operations don’t require their own threads. Instead, they can run on tasks, as when we used trpl::spawn_task to kick off work from a synchronous function.
Definition 17.22 (Task). A unit of concurrent work similar to a thread, but managed by library-level code — the runtime — instead of by the operating system.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
110
Chapter 17 · 17.6 Putting It All Together: Futures, Tasks, and Threads
Threads, tasks, and futures
Table 17.8: Three units of concurrency — and why the APIs for spawning threads and tasks are so similar
| Unit | Managed by | Acts as |
|---|---|---|
| Thread | the operating system | boundary for synchronous operations; concurrency between threads |
| Task | the runtime’s executor | boundary for async operations; concurrency between and within tasks |
| Future | a task | the most granular unit; may represent a tree of other futures |
A task can switch between futures in its body, so tasks are like lightweight, runtime-managed threads, with added capabilities that come from being managed by a runtime instead of by the operating system.
This doesn’t mean that async tasks are always better than threads (or vice versa). Concurrency with threads is in some ways a simpler programming model than concurrency with async, which can be a strength or a weakness. Threads are somewhat “fire and forget”: they have no native equivalent to a future, so they simply run to completion without being interrupted except by the operating system itself.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
111
Chapter 17 · 17.6 Putting It All Together: Futures, Tasks, and Threads
Work stealing, and rules of thumb
Threads and tasks often work very well together, because tasks can (at least in some runtimes) be moved around between threads. In fact, under the hood, the runtime we’ve been using — including the spawn_blocking and spawn_task functions — is multithreaded by default!
Definition 17.23 (Work stealing). An approach many runtimes use to transparently move tasks around between threads, based on how the threads are currently being utilized, to improve the system’s overall performance. It requires threads and tasks, and therefore futures.
When thinking about which method to use when, consider these rules of thumb:
- If the work is very parallelizable (that is, CPU-bound), such as processing a bunch of data where each part can be processed separately, threads are a better choice.
- If the work is very concurrent (that is, I/O-bound), such as handling messages from a bunch of different sources that may come in at different intervals or different rates, async is a better choice.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
112
Chapter 17 · 17.6 Putting It All Together: Futures, Tasks, and Threads
Combining a thread with async
If you need both parallelism and concurrency, you don’t have to choose between threads and async: use them together freely, letting each play the part it’s best at. Listing 17.25 shows a fairly common example of this kind of mix in real-world Rust code.
Listing 17.25: Sending messages with blocking code in a thread and awaiting the messages in an async block — src/main.rs
use std::{thread, time::Duration};
fn main() {
let (tx, mut rx) = trpl::channel();
thread::spawn(move || {
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::block_on(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
113
Chapter 17 · 17.6 Putting It All Together: Futures, Tasks, and Threads
Blocking work on a thread, awaited in async
- We begin by creating an async channel.
- We spawn a thread that takes ownership of the sender side of the channel using the move keyword. Within the thread, we send the numbers 1 through 10, sleeping for a second between each.
- Finally, we run a future created with an async block passed to trpl::block_on, just as throughout the chapter. In that future, we await those messages, just as in the other message-passing examples.
To return to the scenario we opened the chapter with: imagine running a set of video encoding tasks using a dedicated thread (because video encoding is compute-bound) but notifying the UI that those operations are done with an async channel. There are countless examples of these kinds of combinations in real-world use cases.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
114
Chapter 17 · Summary
Chapter summary
This isn’t the last of concurrency in this book: the Chapter 21 project applies these concepts more realistically and compares threading with tasks and futures more directly.
Whichever approach you choose, Rust gives you the tools to write safe, fast, concurrent code — for a high-throughput web server or an embedded OS. You learned:
- futures, async and await, and running them with a runtime (trpl::block_on);
- concurrency with tasks, join/join!, select, async channels and async move;
- yielding control with await points, and composing futures into new abstractions such as timeout;
- streams, and the Future, Stream, StreamExt, Pin and Unpin machinery underneath;
- how futures, tasks and threads fit together, and when to use which.
Remark. Next, Chapter 18 talks about idiomatic ways to model problems and structure solutions as your Rust programs get bigger, and how Rust’s idioms relate to those you might know from object-oriented programming.
The Rust Programming Language · Ch.17 Async, Await, Futures, and Streams
115