مسدود کردن executor

اکثر async runtimeهای تنها به IO task اجازه می‌دهند که به صورت همزمان (concurrent) اجرا شوند. این بدان معنی است که تسک‌های block کردن CPU باعث مسدود شدن executor و جلوگیری از اجرای سایر تسک‌ها می‌شود. یک راه حل آسان این است که در صورت امکان از متدهای معادل async استفاده کنید.

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;
}
This slide should take about 10 minutes.
  • کد را اجرا کنید و ببینید که sleep‌ها به طور متوالی اتفاق می‌افتند و نه به صورت همزمان (concurrent).

  • این "current_thread" همه taskها را روی یک thread قرار می‌دهد. این اثرگذاری را آشکارتر می‌کند، اما این اشکال همچنان در طبیعت multi-threaded وجود دارد.

  • std::thread::sleep را به tokio::time::sleep تغییر دهید و منتظر نتیجه باشید.

  • راه‌حل دیگر tokio::task::spawn_blocking است که یک thread واقعی ایجاد می‌کند و handle آن را بدون مسدود کردن executor به future تبدیل می‌کند.

  • شما نباید taskها را به عنوان thread‌های سیستم عامل در نظر بگیرید. آنها از نگاشت ۱ به ۱ پشتیبانی نمی‌کنند و اکثر executorها به بسیاری از taskها اجازه می‌دهند روی یک thread سیستم عامل اجرا شوند. این امر به‌ویژه هنگام تعامل با کتابخانه‌های دیگر از طریق FFI مشکل‌ساز است، جایی که آن کتابخانه ممکن است به ذخیره‌سازی محلی thread یا نگاشت (map) به threadهای سیستم‌عامل خاص (مانند CUDA) بستگی داشته باشد. در چنین شرایطی tokio::task::spawn_blocking را ترجیح دهید.

  • با احتیاط از همگام‌سازی mutexها استفاده کنید. نگه داشتن یک mutex روی یک .await ممکن است باعث مسدود شدن task دیگری شود و آن task ممکن است در همان thread در حال اجرا باشد.