Блокировка исполнителя
Большинство асинхронных рантаймов позволяют одновременно выполняться только задачам ввода-вывода. Это значит, что задачи, блокирующие ЦП, заблокируют исполнитель и не дадут выполняться другим задачам. Простой обходной путь — использовать асинхронные эквивалентные методы, где это возможно.
// 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;
}
-
Запустите код и убедитесь, что паузы выполняются последовательно, а не одновременно.
-
Вариант
"current_thread"помещает все задачи в один поток. Это делает эффект более очевидным, но ошибка сохраняется и в многопоточном варианте. -
Замените
std::thread::sleepнаtokio::time::sleepи дождитесь его результата через.await. -
Другое решение —
tokio::task::spawn_blocking, который создаёт настоящий поток и превращает его дескриптор во фьючерс, не блокируя исполнитель. -
Не стоит считать задачи потоками ОС. Они не соответствуют друг другу 1 к 1, и исполнители позволяют нескольким задачам выполняться в одном потоке ОС. Это особенно проблемно при взаимодействии с другими библиотеками через FFI, где библиотека может зависеть от локального хранилища потока или сопоставлять себя с конкретными потоками ОС (например, CUDA). В таких ситуациях предпочитайте
tokio::task::spawn_blocking. -
Используйте синхронные мьютексы осторожно. Удержание мьютекса через
.awaitможет заблокировать другую задачу, а она может выполняться в том же потоке.