Блокування виконавця
Більшість асинхронних середовищ виконання дозволяють лише одночасний запуск завдань вводу/виводу. Це означає, що завдання, що блокують процесор, блокуватимуть виконавця та запобігатимуть виконанню інших завдань. Простим обхідним шляхом є використання еквівалентних асинхронних методів, де це можливо.
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!( "ф'ючерс {id} спав протягом {duration_ms}ms, закінчив після {}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
і дочекайтеся результату. -
Іншим виправленням було б
tokio::task::spawn_blocking
, який породжує фактичний потік і перетворює його дескриптор у ф'ючерс, не блокуючи виконавця. -
Ви не повинні думати про завдання як про потоки ОС. Вони не відображаються 1 до 1, і більшість виконавців дозволять виконувати багато завдань в одному потоці ОС. Це особливо проблематично під час взаємодії з іншими бібліотеками через FFI, де ця бібліотека може залежати від локального сховища потоку або зіставлятися з певними потоками ОС (наприклад, CUDA). У таких ситуаціях віддайте перевагу
tokio::task::spawn_blocking
. -
Обережно використовуйте м’ютекси синхронізації. Утримування м'ютексу над
.await
може призвести до блокування іншого завдання, яке може виконуватися в тому самому потоці.