@@ -32,9 +32,9 @@ async fn http_call(v: &[u64]) {
|
||||
|
||||
### `std::sync::MutexGuard` and yield points
|
||||
|
||||
This code will compile, but it's dangerous.
|
||||
This code will compile, but it's dangerous.
|
||||
|
||||
We try to acquire a lock over a `Mutex` from `std` in an asynchronous context.
|
||||
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`).
|
||||
|
||||
@@ -42,18 +42,18 @@ Let's imagine that there are two tasks executing `run`, concurrently, on a singl
|
||||
runtime. We observe the following sequence of scheduling events:
|
||||
|
||||
```text
|
||||
Task A Task B
|
||||
|
|
||||
Acquire lock
|
||||
Yields to runtime
|
||||
|
|
||||
+--------------+
|
||||
|
|
||||
Tries to acquire lock
|
||||
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
|
||||
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`
|
||||
@@ -73,32 +73,32 @@ async fn run(m: Arc<Mutex<Vec<u64>>>) {
|
||||
```
|
||||
|
||||
Acquiring the lock is now an asynchronous operation, which yields back to the runtime
|
||||
if it can't make progress.
|
||||
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
|
||||
[...]
|
||||
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!
|
||||
@@ -107,14 +107,14 @@ All good!
|
||||
|
||||
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.
|
||||
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.
|
||||
would need `N+1` tasks, where `N` is the number of runtime threads.
|
||||
|
||||
### Downsides
|
||||
|
||||
Having an async-aware `Mutex` comes with a performance penalty.
|
||||
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.
|
||||
@@ -124,6 +124,6 @@ will incur.
|
||||
|
||||
## Other primitives
|
||||
|
||||
We used `Mutex` as an example, but the same applies to `RwLock`, semaphores, etc.
|
||||
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.
|
||||
the risk of issues.
|
||||
|
||||
Reference in New Issue
Block a user