Блокування виконавця

Більшість асинхронних середовищ виконання дозволяють лише одночасний запуск завдань вводу/виводу. Це означає, що завдання, що блокують процесор, блокуватимуть виконавця та запобігатимуть виконанню інших завдань. Простим обхідним шляхом є використання еквівалентних асинхронних методів, де це можливо.

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;
}
This slide should take about 10 minutes.
  • Запустіть код і подивіться, що засинання відбуваються послідовно, а не одночасно.

  • Варіант "current_thread" поміщає всі завдання в один потік. Це робить ефект більш очевидним, але помилка все ще присутня в багатопоточному варіанті.

  • Переключіть std::thread::sleep на tokio::time::sleep і дочекайтеся результату.

  • Іншим виправленням було б tokio::task::spawn_blocking, який породжує фактичний потік і перетворює його дескриптор у ф'ючерс, не блокуючи виконавця.

  • Ви не повинні думати про завдання як про потоки ОС. Вони не відображаються 1 до 1, і більшість виконавців дозволять виконувати багато завдань в одному потоці ОС. Це особливо проблематично під час взаємодії з іншими бібліотеками через FFI, де ця бібліотека може залежати від локального сховища потоку або зіставлятися з певними потоками ОС (наприклад, CUDA). У таких ситуаціях віддайте перевагу tokio::task::spawn_blocking.

  • Обережно використовуйте м’ютекси синхронізації. Утримування м'ютексу над .await може призвести до блокування іншого завдання, яке може виконуватися в тому самому потоці.