src/concurrency/async-pitfalls/blocking-executor.md
Most async runtimes only allow IO tasks to run concurrently. This means that CPU blocking tasks will block the executor and prevent other tasks from being executed. An easy workaround is to use async equivalent methods where possible.
# // Copyright 2024 Google LLC
# // SPDX-License-Identifier: Apache-2.0
#
use futures::future::join_all;
use std::time::Instant;
async fn sleep_ms(start: &Instant, id: u64, duration_ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(duration_ms));
println!(
"future {id} slept for {duration_ms}ms, finished after {}ms",
start.elapsed().as_millis()
);
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let start = Instant::now();
let sleep_futures = (1..=10).map(|t| sleep_ms(&start, t, t * 10));
join_all(sleep_futures).await;
}
Run the code and see that the sleeps happen consecutively rather than concurrently.
The "current_thread" flavor puts all tasks on a single thread. This makes
the effect more obvious, but the bug is still present in the multi-threaded
flavor.
Switch the std::thread::sleep to tokio::time::sleep and await its result.
Another fix would be to tokio::task::spawn_blocking which spawns an actual
thread and transforms its handle into a future without blocking the executor.
You should not think of tasks as OS threads. They do not map 1 to 1 and
executors will allow multiple tasks to run on a single OS thread. This is
particularly problematic when interacting with other libraries via FFI, where
that library might depend on thread-local storage or map to specific OS
threads (e.g., CUDA). Prefer tokio::task::spawn_blocking in such situations.
Use sync mutexes with care. Holding a mutex over an .await may cause another
task to block, and that task may be running on the same thread.