1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-12-03 09:45:17 +02:00

Update Concurrency course with times (#2007)

As I mentioned in #1536:

* Break into segments at approximately the places @fw-immunant put
breaks
 * Move all of the files into `src/concurrency`
 * Add timings and segment/session metadata so course outlines appear

There's room for more work here, including some additional feedback from
@fw-immunant after the session I observed, but let's do one step at a
time :)
This commit is contained in:
Dustin J. Mitchell
2024-04-23 09:26:41 -04:00
committed by GitHub
parent a03b7e68e5
commit face5af783
58 changed files with 385 additions and 246 deletions

View File

@@ -353,46 +353,50 @@
---
- [Welcome](concurrency.md)
- [Welcome](concurrency/welcome.md)
- [Threads](concurrency/threads.md)
- [Scoped Threads](concurrency/scoped-threads.md)
- [Plain Threads](concurrency/threads/plain.md)
- [Scoped Threads](concurrency/threads/scoped.md)
- [Channels](concurrency/channels.md)
- [Senders and Reveivers](concurrency/channels/senders-receivers.md)
- [Unbounded Channels](concurrency/channels/unbounded.md)
- [Bounded Channels](concurrency/channels/bounded.md)
- [`Send` and `Sync`](concurrency/send-sync.md)
- [Marker Traits](concurrency/send-sync/marker-traits.md)
- [`Send`](concurrency/send-sync/send.md)
- [`Sync`](concurrency/send-sync/sync.md)
- [Examples](concurrency/send-sync/examples.md)
- [Shared State](concurrency/shared_state.md)
- [`Arc`](concurrency/shared_state/arc.md)
- [`Mutex`](concurrency/shared_state/mutex.md)
- [Example](concurrency/shared_state/example.md)
- [Exercises](exercises/concurrency/morning.md)
- [Dining Philosophers](exercises/concurrency/dining-philosophers.md)
- [Multi-threaded Link Checker](exercises/concurrency/link-checker.md)
- [Solutions](exercises/concurrency/solutions-morning.md)
- [Shared State](concurrency/shared-state.md)
- [`Arc`](concurrency/shared-state/arc.md)
- [`Mutex`](concurrency/shared-state/mutex.md)
- [Example](concurrency/shared-state/example.md)
- [Exercises](concurrency/sync-exercises.md)
- [Dining Philosophers](concurrency/sync-exercises/dining-philosophers.md)
- [Multi-threaded Link Checker](concurrency/sync-exercises/link-checker.md)
- [Solutions](concurrency/sync-exercises/solutions.md)
# Concurrency: Afternoon
- [Async Basics](async.md)
- [`async`/`await`](async/async-await.md)
- [Futures](async/futures.md)
- [Runtimes](async/runtimes.md)
- [Tokio](async/runtimes/tokio.md)
- [Tasks](async/tasks.md)
- [Async Channels](async/channels.md)
- [Control Flow](async/control-flow.md)
- [Join](async/control-flow/join.md)
- [Select](async/control-flow/select.md)
- [Pitfalls](async/pitfalls.md)
- [Blocking the Executor](async/pitfalls/blocking-executor.md)
- [`Pin`](async/pitfalls/pin.md)
- [Async Traits](async/pitfalls/async-traits.md)
- [Cancellation](async/pitfalls/cancellation.md)
- [Exercises](exercises/concurrency/afternoon.md)
- [Dining Philosophers](exercises/concurrency/dining-philosophers-async.md)
- [Broadcast Chat Application](exercises/concurrency/chat-app.md)
- [Solutions](exercises/concurrency/solutions-afternoon.md)
- [Welcome](concurrency/welcome-async.md)
- [Async Basics](concurrency/async.md)
- [`async`/`await`](concurrency/async/async-await.md)
- [Futures](concurrency/async/futures.md)
- [Runtimes](concurrency/async/runtimes.md)
- [Tokio](concurrency/async/runtimes/tokio.md)
- [Tasks](concurrency/async/tasks.md)
- [Channels and Control Flow](concurrency/async-control-flow.md)
- [Async Channels](concurrency/async-control-flow/channels.md)
- [Join](concurrency/async-control-flow/join.md)
- [Select](concurrency/async-control-flow/select.md)
- [Pitfalls](concurrency/async-pitfalls.md)
- [Blocking the Executor](concurrency/async-pitfalls/blocking-executor.md)
- [`Pin`](concurrency/async-pitfalls/pin.md)
- [Async Traits](concurrency/async-pitfalls/async-traits.md)
- [Cancellation](concurrency/async-pitfalls/cancellation.md)
- [Exercises](concurrency/async-exercises.md)
- [Dining Philosophers](concurrency/async-exercises/dining-philosophers.md)
- [Broadcast Chat Application](concurrency/async-exercises/chat-app.md)
- [Solutions](concurrency/async-exercises/solutions.md)
# Final Words

View File

@@ -1,7 +0,0 @@
# Futures Control Flow
Futures can be combined together to produce concurrent compute flow graphs. We
have already seen tasks, that function as independent threads of execution.
- [Join](control-flow/join.md)
- [Select](control-flow/select.md)

View File

@@ -0,0 +1,3 @@
# Channels and Control Flow
{{%segment outline}}

View File

@@ -1,3 +1,7 @@
---
minutes: 8
---
# Async Channels
Several crates have support for asynchronous channels. For instance `tokio`:

View File

@@ -1,3 +1,7 @@
---
minutes: 4
---
# Join
A join operation waits until all of a set of futures are ready, and returns a

View File

@@ -1,3 +1,7 @@
---
minutes: 5
---
# Select
A select operation waits until any of a set of futures is ready, and responds to

View File

@@ -0,0 +1,3 @@
# Exercises
{{%segment outline}}

View File

@@ -0,0 +1 @@
# Exercises

View File

@@ -1,3 +1,7 @@
---
minutes: 30
---
# Broadcast Chat Application
In this exercise, we want to use our new knowledge to implement a broadcast chat

View File

@@ -1,3 +1,7 @@
---
minutes: 20
---
# Dining Philosophers --- Async
See [dining philosophers](dining-philosophers.md) for a description of the
@@ -11,17 +15,17 @@ code below to a file called `src/main.rs`, fill out the blanks, and test that
<!-- File src/main.rs -->
```rust,compile_fail
{{#include dining-philosophers-async.rs:Philosopher}}
{{#include dining-philosophers.rs:Philosopher}}
// left_fork: ...
// right_fork: ...
// thoughts: ...
}
{{#include dining-philosophers-async.rs:Philosopher-think}}
{{#include dining-philosophers.rs:Philosopher-think}}
{{#include dining-philosophers-async.rs:Philosopher-eat}}
{{#include dining-philosophers-async.rs:Philosopher-eat-body}}
{{#include dining-philosophers-async.rs:Philosopher-eat-end}}
{{#include dining-philosophers.rs:Philosopher-eat}}
{{#include dining-philosophers.rs:Philosopher-eat-body}}
{{#include dining-philosophers.rs:Philosopher-eat-end}}
// Create forks
// Create philosophers

View File

@@ -1,17 +1,17 @@
# Concurrency Afternoon Exercise
---
minutes: 20
---
# Solutions
## Dining Philosophers --- Async
([back to exercise](dining-philosophers-async.md))
```rust,compile_fail
{{#include dining-philosophers-async.rs:solution}}
{{#include dining-philosophers.rs:solution}}
```
## Broadcast Chat Application
([back to exercise](chat-app.md))
_src/bin/server.rs_:
```rust,compile_fail

View File

@@ -1,10 +1,7 @@
# Pitfalls of async/await
# Pitfalls
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:
its share of pitfalls and footguns. We illustrate some of them in this chapter.
- [Blocking the Executor](pitfalls/blocking-executor.md)
- [Pin](pitfalls/pin.md)
- [Async Traits](pitfalls/async-traits.md)
- [Cancellation](pitfalls/cancellation.md)
{{%segment outline}}

View File

@@ -1,3 +1,7 @@
---
minutes: 5
---
# Async Traits
Async methods in traits are were stabilized only recently, in the 1.75 release.

View File

@@ -1,3 +1,7 @@
---
minutes: 10
---
# Blocking the executor
Most async runtimes only allow IO tasks to run concurrently. This means that CPU

View File

@@ -1,3 +1,7 @@
---
minutes: 18
---
# Cancellation
Dropping a future implies it can never be polled again. This is called

View File

@@ -1,3 +1,7 @@
---
minutes: 20
---
# `Pin`
Async blocks and functions return types implementing the `Future` trait. The

3
src/concurrency/async.md Normal file
View File

@@ -0,0 +1,3 @@
# Async Basics
{{%segment outline}}

View File

@@ -1,3 +1,7 @@
---
minutes: 6
---
# `async`/`await`
At a high level, async Rust code looks very much like "normal" sequential code:

View File

@@ -1,3 +1,7 @@
---
minutes: 4
---
# Futures
[`Future`](https://doc.rust-lang.org/std/future/trait.Future.html) is a trait,

View File

@@ -1,3 +1,7 @@
---
minutes: 10
---
# Runtimes
A _runtime_ provides support for performing operations asynchronously (a

View File

@@ -1,3 +1,7 @@
---
minutes: 6
---
# Tasks
Rust has a task system, which is a form of lightweight threading.

View File

@@ -1,32 +1,3 @@
# Channels
Rust channels have two parts: a `Sender<T>` and a `Receiver<T>`. The two parts
are connected via the channel, but you only see the end-points.
```rust,editable
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
tx.send(10).unwrap();
tx.send(20).unwrap();
println!("Received: {:?}", rx.recv());
println!("Received: {:?}", rx.recv());
let tx2 = tx.clone();
tx2.send(30).unwrap();
println!("Received: {:?}", rx.recv());
}
```
<details>
- `mpsc` stands for Multi-Producer, Single-Consumer. `Sender` and `SyncSender`
implement `Clone` (so you can make multiple producers) but `Receiver` does
not.
- `send()` and `recv()` return `Result`. If they return `Err`, it means the
counterpart `Sender` or `Receiver` is dropped and the channel is closed.
</details>
{{%segment outline}}

View File

@@ -1,3 +1,7 @@
---
minutes: 8
---
# Bounded Channels
With bounded (synchronous) channels, `send` can block the current thread:

View File

@@ -0,0 +1,36 @@
---
minutes: 9
---
# Senders and Receivers
Rust channels have two parts: a `Sender<T>` and a `Receiver<T>`. The two parts
are connected via the channel, but you only see the end-points.
```rust,editable
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
tx.send(10).unwrap();
tx.send(20).unwrap();
println!("Received: {:?}", rx.recv());
println!("Received: {:?}", rx.recv());
let tx2 = tx.clone();
tx2.send(30).unwrap();
println!("Received: {:?}", rx.recv());
}
```
<details>
- `mpsc` stands for Multi-Producer, Single-Consumer. `Sender` and `SyncSender`
implement `Clone` (so you can make multiple producers) but `Receiver` does
not.
- `send()` and `recv()` return `Result`. If they return `Err`, it means the
counterpart `Sender` or `Receiver` is dropped and the channel is closed.
</details>

View File

@@ -1,3 +1,7 @@
---
minutes: 2
---
# Unbounded Channels
You get an unbounded and asynchronous channel with `mpsc::channel()`:

View File

@@ -1,25 +1,3 @@
# `Send` and `Sync`
How does Rust know to forbid shared access across threads? The answer is in two
traits:
- [`Send`][1]: a type `T` is `Send` if it is safe to move a `T` across a thread
boundary.
- [`Sync`][2]: a type `T` is `Sync` if it is safe to move a `&T` across a thread
boundary.
`Send` and `Sync` are [unsafe traits][3]. The compiler will automatically derive
them for your types as long as they only contain `Send` and `Sync` types. You
can also implement them manually when you know it is valid.
[1]: https://doc.rust-lang.org/std/marker/trait.Send.html
[2]: https://doc.rust-lang.org/std/marker/trait.Sync.html
[3]: ../unsafe/unsafe-traits.md
<details>
- One can think of these traits as markers that the type has certain
thread-safety properties.
- They can be used in the generic constraints as normal traits.
</details>
{{%segment outline}}

View File

@@ -1,3 +1,7 @@
---
minutes: 6
---
# Examples
## `Send + Sync`

View File

@@ -0,0 +1,29 @@
---
minutes: 2
---
# Marker Traits
How does Rust know to forbid shared access across threads? The answer is in two
traits:
- [`Send`][1]: a type `T` is `Send` if it is safe to move a `T` across a thread
boundary.
- [`Sync`][2]: a type `T` is `Sync` if it is safe to move a `&T` across a thread
boundary.
`Send` and `Sync` are [unsafe traits][3]. The compiler will automatically derive
them for your types as long as they only contain `Send` and `Sync` types. You
can also implement them manually when you know it is valid.
[1]: https://doc.rust-lang.org/std/marker/trait.Send.html
[2]: https://doc.rust-lang.org/std/marker/trait.Sync.html
[3]: ../unsafe/unsafe-traits.md
<details>
- One can think of these traits as markers that the type has certain
thread-safety properties.
- They can be used in the generic constraints as normal traits.
</details>

View File

@@ -1,3 +1,7 @@
---
minutes: 2
---
# `Send`
> A type `T` is [`Send`][1] if it is safe to move a `T` value to another thread.

View File

@@ -1,3 +1,7 @@
---
minutes: 2
---
# `Sync`
> A type `T` is [`Sync`][1] if it is safe to access a `T` value from multiple

View File

@@ -0,0 +1,3 @@
# Shared State
{{%segment outline}}

View File

@@ -1,3 +1,7 @@
---
minutes: 5
---
# `Arc`
[`Arc<T>`][1] allows shared read-only access via `Arc::clone`:

View File

@@ -1,3 +1,7 @@
---
minutes: 8
---
# Example
Let us see `Arc` and `Mutex` in action:

View File

@@ -1,3 +1,7 @@
---
minutes: 14
---
# `Mutex`
[`Mutex<T>`][1] ensures mutual exclusion _and_ allows mutable access to `T`

View File

@@ -1,11 +0,0 @@
# Shared State
Rust uses the type system to enforce synchronization of shared data. This is
primarily done via two types:
- [`Arc<T>`][1], atomic reference counted `T`: handles sharing between threads
and takes care to deallocate `T` when the last reference is dropped,
- [`Mutex<T>`][2]: ensures mutually exclusive access to the `T` value.
[1]: https://doc.rust-lang.org/std/sync/struct.Arc.html
[2]: https://doc.rust-lang.org/std/sync/struct.Mutex.html

View File

@@ -0,0 +1,3 @@
# Exercises
{{%segment outline}}

View File

@@ -1,16 +1,16 @@
[package]
name = "comprehensive-rust"
name = "sync-exercises"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "dining-philosophers"
path = "concurrency/dining-philosophers.rs"
path = "dining-philosophers.rs"
[[bin]]
name = "link-checker"
path = "concurrency/link-checker.rs"
path = "link-checker.rs"
[dependencies]
reqwest = { version = "0.12.4", features = ["blocking"] }

View File

@@ -1,3 +1,7 @@
---
minutes: 20
---
# Dining Philosophers
The dining philosophers problem is a classic problem in concurrency:

View File

@@ -1,3 +1,7 @@
---
minutes: 20
---
# Multi-threaded Link Checker
Let us use our new knowledge to create a multi-threaded link checker. It should

View File

@@ -1,17 +1,17 @@
# Concurrency Morning Exercise
---
minutes: 30
---
# Solutions
## Dining Philosophers
([back to exercise](dining-philosophers.md))
```rust
{{#include dining-philosophers.rs:solution}}
```
## Link Checker
([back to exercise](link-checker.md))
```rust,compile_fail
{{#include link-checker.rs:solution}}
```

View File

@@ -1,74 +1,3 @@
# Threads
Rust threads work similarly to threads in other languages:
```rust,editable
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("Count in thread: {i}!");
thread::sleep(Duration::from_millis(5));
}
});
for i in 1..5 {
println!("Main thread: {i}");
thread::sleep(Duration::from_millis(5));
}
}
```
- Threads are all daemon threads, the main thread does not wait for them.
- Thread panics are independent of each other.
- Panics can carry a payload, which can be unpacked with `downcast_ref`.
<details>
- Rust thread APIs look not too different from e.g. C++ ones.
- Run the example.
- 5ms timing is loose enough that main and spawned threads stay mostly in
lockstep.
- Notice that the program ends before the spawned thread reaches 10!
- This is because main ends the program and spawned threads do not make it
persist.
- Compare to pthreads/C++ std::thread/boost::thread if desired.
- How do we wait around for the spawned thread to complete?
- [`thread::spawn`] returns a `JoinHandle`. Look at the docs.
- `JoinHandle` has a [`.join()`] method that blocks.
- Use `let handle = thread::spawn(...)` and later `handle.join()` to wait for
the thread to finish and have the program count all the way to 10.
- Now what if we want to return a value?
- Look at docs again:
- [`thread::spawn`]'s closure returns `T`
- `JoinHandle` [`.join()`] returns `thread::Result<T>`
- Use the `Result` return value from `handle.join()` to get access to the
returned value.
- Ok, what about the other case?
- Trigger a panic in the thread. Note that this doesn't panic `main`.
- Access the panic payload. This is a good time to talk about [`Any`].
- Now we can return values from threads! What about taking inputs?
- Capture something by reference in the thread closure.
- An error message indicates we must move it.
- Move it in, see we can compute and then return a derived value.
- If we want to borrow?
- Main kills child threads when it returns, but another function would just
return and leave them running.
- That would be stack use-after-return, which violates memory safety!
- How do we avoid this? see next slide.
[`Any`]: https://doc.rust-lang.org/std/any/index.html
[`thread::spawn`]: https://doc.rust-lang.org/std/thread/fn.spawn.html
[`.join()`]: https://doc.rust-lang.org/std/thread/struct.JoinHandle.html#method.join
</details>
{{%segment outline}}

View File

@@ -0,0 +1,78 @@
---
minutes: 15
---
# Plain Threads
Rust threads work similarly to threads in other languages:
```rust,editable
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("Count in thread: {i}!");
thread::sleep(Duration::from_millis(5));
}
});
for i in 1..5 {
println!("Main thread: {i}");
thread::sleep(Duration::from_millis(5));
}
}
```
- Threads are all daemon threads, the main thread does not wait for them.
- Thread panics are independent of each other.
- Panics can carry a payload, which can be unpacked with `downcast_ref`.
<details>
- Rust thread APIs look not too different from e.g. C++ ones.
- Run the example.
- 5ms timing is loose enough that main and spawned threads stay mostly in
lockstep.
- Notice that the program ends before the spawned thread reaches 10!
- This is because main ends the program and spawned threads do not make it
persist.
- Compare to pthreads/C++ std::thread/boost::thread if desired.
- How do we wait around for the spawned thread to complete?
- [`thread::spawn`] returns a `JoinHandle`. Look at the docs.
- `JoinHandle` has a [`.join()`] method that blocks.
- Use `let handle = thread::spawn(...)` and later `handle.join()` to wait for
the thread to finish and have the program count all the way to 10.
- Now what if we want to return a value?
- Look at docs again:
- [`thread::spawn`]'s closure returns `T`
- `JoinHandle` [`.join()`] returns `thread::Result<T>`
- Use the `Result` return value from `handle.join()` to get access to the
returned value.
- Ok, what about the other case?
- Trigger a panic in the thread. Note that this doesn't panic `main`.
- Access the panic payload. This is a good time to talk about [`Any`].
- Now we can return values from threads! What about taking inputs?
- Capture something by reference in the thread closure.
- An error message indicates we must move it.
- Move it in, see we can compute and then return a derived value.
- If we want to borrow?
- Main kills child threads when it returns, but another function would just
return and leave them running.
- That would be stack use-after-return, which violates memory safety!
- How do we avoid this? see next slide.
[`Any`]: https://doc.rust-lang.org/std/any/index.html
[`thread::spawn`]: https://doc.rust-lang.org/std/thread/fn.spawn.html
[`.join()`]: https://doc.rust-lang.org/std/thread/struct.JoinHandle.html#method.join
</details>

View File

@@ -1,3 +1,7 @@
---
minutes: 13
---
# Scoped Threads
Normal threads cannot borrow from their environment:

View File

@@ -1,8 +1,9 @@
---
session: Afternoon
target_minutes: 180
---
# Async Rust
# Welcome
"Async" is a concurrency model where multiple tasks are executed concurrently by
executing each task until it would block, then switching to another task that is
@@ -27,3 +28,7 @@ available.
- JavaScript's `Promise` is similar, but again callback-based. The language
runtime implements the event loop, so many of the details of Promise
resolution are hidden.
## Schedule
{{%session outline}}

View File

@@ -1,6 +1,7 @@
---
course: Concurrency
session: Morning
target_minutes: 180
---
# Welcome to Concurrency in Rust
@@ -12,6 +13,10 @@ The Rust type system plays an important role in making many concurrency bugs
compile time bugs. This is often referred to as _fearless concurrency_ since you
can rely on the compiler to ensure correctness at runtime.
## Schedule
{{%session outline}}
<details>
- Rust lets us access OS concurrency toolkit: threads, sync. primitives, etc.

View File

@@ -1,17 +0,0 @@
# Exercises
To practice your Async Rust skills, we have again two exercises for you:
- Dining philosophers: we already saw this problem in the morning. This time you
are going to implement it with Async Rust.
- A Broadcast Chat Application: this is a larger project that allows you
experiment with more advanced Async Rust features.
<details>
After looking at the exercises, you can look at the [solutions] provided.
[solutions]: solutions-afternoon.md
</details>

View File

@@ -70,6 +70,8 @@ cargo add tokio --features full
cargo run
```
{{%course outline Concurrency}}
## Format
The course is meant to be very interactive and we recommend letting the