initial commit
This commit is contained in:
79
book/src/08_futures/05_blocking.md
Normal file
79
book/src/08_futures/05_blocking.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Don't block the runtime
|
||||
|
||||
Let's circle back to yield points.\
|
||||
Unlike threads, **Rust tasks cannot be preempted**.
|
||||
|
||||
`tokio` cannot, on its own, decide to pause a task and run another one in its place.
|
||||
The control goes back to the executor **exclusively** when the task yields—i.e.
|
||||
when `Future::poll` returns `Poll::Pending` or, in the case of `async fn`, when
|
||||
you `.await` a future.
|
||||
|
||||
This exposes the runtime to a risk: if a task never yields, the runtime will never
|
||||
be able to run another task. This is called **blocking the runtime**.
|
||||
|
||||
## What is blocking?
|
||||
|
||||
How long is too long? How much time can a task spend without yielding before it
|
||||
becomes a problem?
|
||||
|
||||
It depends on the runtime, the application, the number of in-flight tasks, and
|
||||
many other factors. But, as a general rule of thumb, try to spend less than 100
|
||||
microseconds between yield points.
|
||||
|
||||
## Consequences
|
||||
|
||||
Blocking the runtime can lead to:
|
||||
|
||||
- **Deadlocks**: if the task that's not yielding is waiting for another task to
|
||||
complete, and that task is waiting for the first one to yield, you have a deadlock.
|
||||
No progress can be made, unless the runtime is able to schedule the other task on
|
||||
a different thread.
|
||||
- **Starvation**: other tasks might not be able to run, or might run after a long
|
||||
delay, which can lead to poor performances (e.g. high tail latencies).
|
||||
|
||||
## Blocking is not always obvious
|
||||
|
||||
Some types of operations should generally be avoided in async code, like:
|
||||
|
||||
- Synchronous I/O. You can't predict how long it will take, and it's likely to be
|
||||
longer than 100 microseconds.
|
||||
- Expensive CPU-bound computations.
|
||||
|
||||
The latter category is not always obvious though. For example, sorting a vector with
|
||||
a few elements is not a problem; that evaluation changes if the vector has billions
|
||||
of entries.
|
||||
|
||||
## How to avoid blocking
|
||||
|
||||
OK, so how do you avoid blocking the runtime assuming you _must_ perform an operation
|
||||
that qualifies or risks qualifying as blocking?\
|
||||
You need to move the work to a different thread. You don't want to use the so-called
|
||||
runtime threads, the ones used by `tokio` to run tasks.
|
||||
|
||||
`tokio` provides a dedicated threadpool for this purpose, called the **blocking pool**.
|
||||
You can spawn a synchronous operation on the blocking pool using the
|
||||
`tokio::task::spawn_blocking` function. `spawn_blocking` returns a future that resolves
|
||||
to the result of the operation when it completes.
|
||||
|
||||
```rust
|
||||
use tokio::task;
|
||||
|
||||
fn expensive_computation() -> u64 {
|
||||
// [...]
|
||||
}
|
||||
|
||||
async fn run() {
|
||||
let handle = task::spawn_blocking(expensive_computation);
|
||||
// Do other stuff in the meantime
|
||||
let result = handle.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
The blocking pool is long-lived. `spawn_blocking` should be faster
|
||||
than creating a new thread directly via `std::thread::spawn`
|
||||
because the cost of thread initialization is amortized over multiple calls.
|
||||
|
||||
## Further reading
|
||||
|
||||
- Check out [Alice Ryhl's blog post](https://ryhl.io/blog/async-what-is-blocking/)
|
||||
on the topic.
|
||||
Reference in New Issue
Block a user