Skip to content

Commit 25055b8

Browse files
djmitchesakexrbehjati
authored andcommitted
Add an "async" session (google#496)
* beginning of an Async section * address review comments * Add futures page (google#497) NOTE: `mdbook test` does not allow code samples to reference other crates, so they must be marked as `compile_fail`; see google#175. * Add Runtimes & Tasks (google#522) These concepts are closely related, and there's not much else to know about runtimes other than "they exist". This removes the bit about futures being "inert" because it doesn't really lead anywhere. * Async chapter (google#524) * Add async channels chapter * Async control flow * Async pitfalls * Separate in multiple chapters + add daemon section * Merge reentering threads in blocking-executor * async_trait * Async fixes (google#546) * Async: some ideas for simplifying the content (google#550) * Simplify the async-await slide * Shorten futures and move it up * Add a page on Tokio * Modifications to the async section (google#556) * Modifications to the async section * Remove the "Daemon" slide, as it largely duplicates the "Tasks" slide. The introduction to the "Control Flow" section mentions tasks as a kind of control flow. * Reorganize the structure in SUMMARY.md to correspond to the directory structure. * Simplify the "Pin" and "Blocking the Executor" slides with steps in the speaker notes to demonstrate / fix the issues. * Rename "join_all" to "Join". * Simplify some code samples to shorten them, and to print output rather than asserting. * Clarify speaker notes and include more "Try.." suggestions. * Be consistent about where `async` blocks are introduced (in the "Tasks" slide). * Explain `join` and `select` in prose. * Fix formatting of section-header slides. * Add a note on async trait (google#558) --------- Co-authored-by: sakex <[email protected]> Co-authored-by: rbehjati <[email protected]>
1 parent 22729e2 commit 25055b8

File tree

17 files changed

+706
-1
lines changed

17 files changed

+706
-1
lines changed

src/SUMMARY.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,24 @@
226226
- [With Java](android/interoperability/java.md)
227227
- [Exercises](exercises/day-4/android.md)
228228

229-
# Async (temp until issue181 is closed)
229+
# Day 4: Afternoon (Async)
230230

231+
----
232+
233+
- [Async Basics](async.md)
234+
- [async/await](async/async-await.md)
235+
- [Futures](async/futures.md)
236+
- [Runtimes](async/runtimes.md)
237+
- [Tokio](async/runtimes/tokio.md)
238+
- [Tasks](async/tasks.md)
239+
- [Async Channels](async/channels.md)
240+
- [Control Flow](async/control-flow.md)
241+
- [Join](async/control-flow/join.md)
242+
- [Select](async/control-flow/select.md)
243+
- [Pitfalls](async/pitfalls.md)
244+
- [Blocking the Executor](async/pitfalls/blocking-executor.md)
245+
- [Pin](async/pitfalls/pin.md)
246+
- [Async Traits](async/pitfalls/async-traits.md)
231247
- [Exercises](exercises/day-4/elevator.md)
232248

233249
# Final Words

src/async.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Async Rust
2+
3+
"Async" is a concurrency model where multiple tasks are executed concurrently by
4+
executing each task until it would block, then switching to another task that is
5+
ready to make progress. The model allows running a larger number of tasks on a
6+
limited number of threads. This is because the per-task overhead is typically
7+
very low and operating systems provide primitives for efficiently identifying
8+
I/O that is able to proceed.
9+
10+
Rust's asynchronous operation is based on "futures", which represent work that
11+
may be completed in the future. Futures are "polled" until they signal that
12+
they are complete.
13+
14+
Futures are polled by an async runtime, and several different runtimes are
15+
available.
16+
17+
## Comparisons
18+
19+
* Python has a similar model in its `asyncio`. However, its `Future` type is
20+
callback-based, and not polled. Async Python programs require a "loop",
21+
similar to a runtime in Rust.
22+
23+
* JavaScript's `Promise` is similar, but again callback-based. The language
24+
runtime implements the event loop, so many of the details of Promise
25+
resolution are hidden.

src/async/async-await.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# `async`/`await`
2+
3+
At a high level, async Rust code looks very much like "normal" sequential code:
4+
5+
```rust,editable,compile_fail
6+
use futures::executor::block_on;
7+
8+
async fn count_to(count: i32) {
9+
for i in 1..=count {
10+
println!("Count is: {i}!");
11+
}
12+
}
13+
14+
async fn async_main(count: i32) {
15+
count_to(count).await;
16+
}
17+
18+
fn main() {
19+
block_on(async_main(10));
20+
}
21+
```
22+
23+
<details>
24+
25+
Key points:
26+
27+
* Note that this is a simplified example to show the syntax. There is no long
28+
running operation or any real concurrency in it!
29+
30+
* What is the return type of an async call?
31+
* Use `let future: () = async_main(10);` in `main` to see the type.
32+
33+
* The "async" keyword is syntactic sugar. The compiler replaces the return type
34+
with a future.
35+
36+
* You cannot make `main` async, without additional instructions to the compiler
37+
on how to use the returned future.
38+
39+
* You need an executor to run async code. `block_on` blocks the current thread
40+
until the provided future has run to completion.
41+
42+
* `.await` asynchronously waits for the completion of another operation. Unlike
43+
`block_on`, `.await` doesn't block the current thread.
44+
45+
* `.await` can only be used inside an `async` function (or block; these are
46+
introduced later).
47+
48+
</details>

src/async/channels.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Async Channels
2+
3+
Several crates have support for `async`/`await`. For instance `tokio` channels:
4+
5+
```rust,editable,compile_fail
6+
use tokio::sync::mpsc::{self, Receiver};
7+
8+
async fn ping_handler(mut input: Receiver<()>) {
9+
let mut count: usize = 0;
10+
11+
while let Some(_) = input.recv().await {
12+
count += 1;
13+
println!("Received {count} pings so far.");
14+
}
15+
16+
println!("ping_handler complete");
17+
}
18+
19+
#[tokio::main]
20+
async fn main() {
21+
let (sender, receiver) = mpsc::channel(32);
22+
let ping_handler_task = tokio::spawn(ping_handler(receiver));
23+
for i in 0..10 {
24+
sender.send(()).await.expect("Failed to send ping.");
25+
println!("Sent {} pings so far.", i + 1);
26+
}
27+
28+
std::mem::drop(sender);
29+
ping_handler_task.await.expect("Something went wrong in ping handler task.");
30+
}
31+
```
32+
33+
<details>
34+
35+
* Change the channel size to `3` and see how it affects the execution.
36+
37+
* Overall, the interface is similar to the `sync` channels as seen in the
38+
[morning class](concurrency/channels.md).
39+
40+
* Try removing the `std::mem::drop` call. What happens? Why?
41+
42+
* The [Flume](https://docs.rs/flume/latest/flume/) crate has channels that
43+
implement both `sync` and `async` `send` and `recv`. This can be convenient
44+
for complex applications with both IO and heavy CPU processing tasks.
45+
46+
* What makes working with `async` channels preferable is the ability to combine
47+
them with other `future`s to combine them and create complex control flow.
48+
49+
</details>

src/async/control-flow.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Futures Control Flow
2+
3+
Futures can be combined together to produce concurrent compute flow graphs. We
4+
have already seen tasks, that function as independent threads of execution.
5+
6+
- [Join](control-flow/join.md)
7+
- [Select](control-flow/select.md)

src/async/control-flow/join.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Join
2+
3+
A join operation waits until all of a set of futures are ready, and
4+
returns a collection of their results. This is similar to `Promise.all` in
5+
JavaScript or `asyncio.gather` in Python.
6+
7+
```rust,editable,compile_fail
8+
use anyhow::Result;
9+
use futures::future;
10+
use reqwest;
11+
use std::collections::HashMap;
12+
13+
async fn size_of_page(url: &str) -> Result<usize> {
14+
let resp = reqwest::get(url).await?;
15+
Ok(resp.text().await?.len())
16+
}
17+
18+
#[tokio::main]
19+
async fn main() {
20+
let urls: [&str; 4] = [
21+
"https://google.com",
22+
"https://httpbin.org/ip",
23+
"https://play.rust-lang.org/",
24+
"BAD_URL",
25+
];
26+
let futures_iter = urls.into_iter().map(size_of_page);
27+
let results = future::join_all(futures_iter).await;
28+
let page_sizes_dict: HashMap<&str, Result<usize>> =
29+
urls.into_iter().zip(results.into_iter()).collect();
30+
println!("{:?}", page_sizes_dict);
31+
}
32+
```
33+
34+
<details>
35+
36+
Copy this example into your prepared `src/main.rs` and run it from there.
37+
38+
* For multiple futures of disjoint types, you can use `std::future::join!` but
39+
you must know how many futures you will have at compile time. This is
40+
currently in the `futures` crate, soon to be stabilised in `std::future`.
41+
42+
* The risk of `join` is that one of the futures may never resolve, this would
43+
cause your program to stall.
44+
45+
* You can also combine `join_all` with `join!` for instance to join all requests
46+
to an http service as well as a database query.
47+
48+
* Try adding a timeout to the future, using `futures::join!`.
49+
50+
</details>
51+

src/async/control-flow/select.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Select
2+
3+
A select operation waits until any of a set of futures is ready, and responds to
4+
that future's result. In JavaScript, this is similar to `Promise.race`. In
5+
Python, it compares to `asyncio.wait(task_set,
6+
return_when=asyncio.FIRST_COMPLETED)`.
7+
8+
This is usually a macro, similar to match, with each arm of the form `pattern =
9+
future => statement`. When the future is ready, the statement is executed with the
10+
variable bound to the future's result.
11+
12+
```rust,editable,compile_fail
13+
use tokio::sync::mpsc::{self, Receiver};
14+
use tokio::time::{sleep, Duration};
15+
16+
#[derive(Debug, PartialEq)]
17+
enum Animal {
18+
Cat { name: String },
19+
Dog { name: String },
20+
}
21+
22+
async fn first_animal_to_finish_race(
23+
mut cat_rcv: Receiver<String>,
24+
mut dog_rcv: Receiver<String>,
25+
) -> Option<Animal> {
26+
tokio::select! {
27+
cat_name = cat_rcv.recv() => Some(Animal::Cat { name: cat_name? }),
28+
dog_name = dog_rcv.recv() => Some(Animal::Dog { name: dog_name? })
29+
}
30+
}
31+
32+
#[tokio::main]
33+
async fn main() {
34+
let (cat_sender, cat_receiver) = mpsc::channel(32);
35+
let (dog_sender, dog_receiver) = mpsc::channel(32);
36+
tokio::spawn(async move {
37+
sleep(Duration::from_millis(500)).await;
38+
cat_sender
39+
.send(String::from("Felix"))
40+
.await
41+
.expect("Failed to send cat.");
42+
});
43+
tokio::spawn(async move {
44+
sleep(Duration::from_millis(50)).await;
45+
dog_sender
46+
.send(String::from("Rex"))
47+
.await
48+
.expect("Failed to send dog.");
49+
});
50+
51+
let winner = first_animal_to_finish_race(cat_receiver, dog_receiver)
52+
.await
53+
.expect("Failed to receive winner");
54+
55+
println!("Winner is {winner:?}");
56+
}
57+
```
58+
59+
<details>
60+
61+
* In this example, we have a race between a cat and a dog.
62+
`first_animal_to_finish_race` listens to both channels and will pick whichever
63+
arrives first. Since the dog takes 50ms, it wins against the cat that
64+
take 500ms seconds.
65+
66+
* You can use `oneshot` channels in this example as the channels are supposed to
67+
receive only one `send`.
68+
69+
* Try adding a deadline to the race, demonstrating selecting different sorts of
70+
futures.
71+
72+
* Note that `select!` consumes the futures it is given, and is easiest to use
73+
when every execution of `select!` creates new futures.
74+
75+
</details>

src/async/futures.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Futures
2+
3+
[`Future`](https://doc.rust-lang.org/std/future/trait.Future.html)
4+
is a trait, implemented by objects that represent an operation that may not be
5+
complete yet. A future can be polled, and `poll` returns a
6+
[`Poll`](https://doc.rust-lang.org/std/task/enum.Poll.html).
7+
8+
```rust
9+
use std::pin::Pin;
10+
use std::task::Context;
11+
12+
pub trait Future {
13+
type Output;
14+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
15+
}
16+
17+
pub enum Poll<T> {
18+
Ready(T),
19+
Pending,
20+
}
21+
```
22+
23+
An async function returns an `impl Future`. It's also possible (but uncommon) to
24+
implement `Future` for your own types. For example, the `JoinHandle` returned
25+
from `tokio::spawn` implements `Future` to allow joining to it.
26+
27+
The `.await` keyword, applied to a Future, causes the current async function to
28+
pause until that Future is ready, and then evaluates to its output.
29+
30+
<details>
31+
32+
* The `Future` and `Poll` types are implemented exactly as shown; click the
33+
links to show the implementations in the docs.
34+
35+
* We will not get to `Pin` and `Context`, as we will focus on writing async
36+
code, rather than building new async primitives. Briefly:
37+
38+
* `Context` allows a Future to schedule itself to be polled again when an
39+
event occurs.
40+
41+
* `Pin` ensures that the Future isn't moved in memory, so that pointers into
42+
that future remain valid. This is required to allow references to remain
43+
valid after an `.await`.
44+
45+
</details>

src/async/pitfalls.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Pitfalls of async/await
2+
3+
Async / await provides convenient and efficient abstraction for concurrent asynchronous programming. However, the async/await model in Rust also comes with its share of pitfalls and footguns. We illustrate some of them in this chapter:
4+
5+
- [Blocking the Executor](pitfalls/blocking-executor.md)
6+
- [Pin](pitfalls/pin.md)
7+
- [Async Traits](pitfall/async-traits.md)

0 commit comments

Comments
 (0)