Last chapter on async Rust
This commit is contained in:
129
book/src/08_futures/06_async_aware_primitives.md
Normal file
129
book/src/08_futures/06_async_aware_primitives.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Async-aware primitives
|
||||
|
||||
If you browse `tokio`'s documentation, you'll notice that it provides a lot of types
|
||||
that "mirror" the ones in the standard library, but with an asynchronous twist:
|
||||
locks, channels, timers, and more.
|
||||
|
||||
When working in an asynchronous context, you should prefer these asynchronous alternatives
|
||||
to their synchronous counterparts.
|
||||
|
||||
To understand why, let's take a look at `Mutex`, the mutually exclusive lock we explored
|
||||
in the previous chapter.
|
||||
|
||||
## Case study: `Mutex`
|
||||
|
||||
Let's look at a simple example:
|
||||
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
async fn run(m: Arc<Mutex<Vec<u64>>>) {
|
||||
let guard = m.lock().unwrap();
|
||||
http_call(&guard).await;
|
||||
println!("Sent {:?} to the server", &guard);
|
||||
// `guard` is dropped here
|
||||
}
|
||||
|
||||
/// Use `v` as the body of an HTTP call.
|
||||
async fn http_call(v: &[u64]) {
|
||||
// [...]
|
||||
}
|
||||
```
|
||||
|
||||
### `std::sync::MutexGuard` and yield points
|
||||
|
||||
This code will compile, but it's dangerous.
|
||||
|
||||
We try to acquire a lock over a `Mutex` from `std` in an asynchronous context.
|
||||
We then hold on to the resulting `MutexGuard` across a yield point (the `.await` on
|
||||
`http_call`).
|
||||
|
||||
Let's imagine that there are two tasks executing `run`, concurrently, on a single-threaded
|
||||
runtime. We observe the following sequence of scheduling events:
|
||||
|
||||
```text
|
||||
Task A Task B
|
||||
|
|
||||
Acquire lock
|
||||
Yields to runtime
|
||||
|
|
||||
+--------------+
|
||||
|
|
||||
Tries to acquire lock
|
||||
```
|
||||
|
||||
We have a deadlock. Task B we'll never manage to acquire the lock, because the lock
|
||||
is currently held by task A, which has yielded to the runtime before releasing the
|
||||
lock and won't be scheduled again because the runtime cannot preempt task B.
|
||||
|
||||
### `tokio::sync::Mutex`
|
||||
|
||||
You can solve the issue by switching to `tokio::sync::Mutex`:
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
async fn run(m: Arc<Mutex<Vec<u64>>>) {
|
||||
let guard = m.lock().await;
|
||||
http_call(&guard).await;
|
||||
println!("Sent {:?} to the server", &guard);
|
||||
// `guard` is dropped here
|
||||
}
|
||||
```
|
||||
|
||||
Acquiring the lock is now an asynchronous operation, which yields back to the runtime
|
||||
if it can't make progress.
|
||||
Going back to the previous scenario, the following would happen:
|
||||
|
||||
```text
|
||||
Task A Task B
|
||||
|
|
||||
Acquires the lock
|
||||
Starts `http_call`
|
||||
Yields to runtime
|
||||
|
|
||||
+--------------+
|
||||
|
|
||||
Tries to acquire the lock
|
||||
Cannot acquire the lock
|
||||
Yields to runtime
|
||||
|
|
||||
+--------------+
|
||||
|
|
||||
`http_call` completes
|
||||
Releases the lock
|
||||
Yield to runtime
|
||||
|
|
||||
+--------------+
|
||||
|
|
||||
Acquires the lock
|
||||
[...]
|
||||
```
|
||||
|
||||
All good!
|
||||
|
||||
### Multithreaded won't save you
|
||||
|
||||
We've used a single-threaded runtime as the execution context in our
|
||||
previous example, but the same risk persists even when using a multithreaded
|
||||
runtime.
|
||||
The only difference is in the number of concurrent tasks required to create the deadlock:
|
||||
in a single-threaded runtime, 2 are enough; in a multithreaded runtime, we
|
||||
would need `N+1` tasks, where `N` is the number of runtime threads.
|
||||
|
||||
### Downsides
|
||||
|
||||
Having an async-aware `Mutex` comes with a performance penalty.
|
||||
If you're confident that the lock isn't under significant contention
|
||||
_and_ you're careful to never hold it across a yield point, you can
|
||||
still use `std::sync::Mutex` in an asynchronous context.
|
||||
|
||||
But weigh the performance benefit against the liveness risk you
|
||||
will incur.
|
||||
|
||||
## Other primitives
|
||||
|
||||
We used `Mutex` as an example, but the same applies to `RwLock`, semaphores, etc.
|
||||
Prefer async-aware versions when working in an asynchronous context to minimise
|
||||
the risk of issues.
|
||||
Reference in New Issue
Block a user