کلیدواژه های async/await

async/.await ابزار های Rust برای نوشتن توابع Async هستن که باعث میشه کد خیلی شبیه به کد sync به نظر بیاد. async یک بلاک کد رو تبدیل میکنه به یک وضعیت یا state machine که trait ای به اسم Future رو ازش استفاده و پیاده سازی کرده. در حالی که صدا زدن یک تابع که که کل thread رو نگه میداره یا اصطلاحا بلاک میکنه، Future های بلاک شده کنترل thread رو بخودش برمیگردونن و باعث میشن Future های دیگه اجرا بشن.

بیاید یه پکیج crate رو به فایل Cargo.toml پروژمون اضافه کنیم:

[dependencies]
futures = "0.3"

برای ساختن یک تابع Async، میتونید از کلیدواژه async fn استفاده کنید:


#![allow(unused)]
fn main() {
async fn do_something() { /* ... */ }
}

چیزی که اون تابع async fn برمیگردونه یک Future هست. برای اینکه در ادامه چیزی اتفاق بیفته، اون Future باید توسط یک اجرا کننده اجرا بشه.

// تابع "بلاک آن" تردی که در آن اجرا می شود را بلاک و مسدود میکند
// و وقتی آزاد می شود که اون "فیوچر"ی که در حال اجرای آن است به پایان برسد
// اجرا کننده های دیگه رفتار های پیچیده تری از خودشون نشون میدن
// مثل زمان بندی برای اجرای چند فیوچر در یک ترد
use futures::executor::block_on;

async fn hello_world() {
    println!("hello, world!");
}

fn main() {
    let future = hello_world(); // اینجا هیچی پرینت نمیشه
    block_on(future); // تابع "فیوچر" اجرا شد و متن بالا پرینت شد
}

داخل یک تابع async fn، میتونید از .await استفاده کنید تا برای تموم شدن اجرای یک type دیگه ای که اونم از نوع Future هست صبر کنید، مثلا خروجی یک تابع async fn دیگه. برعکس block_on، .await اون thread رو بلاک و مسدود نمیکنه، در ازاش به صورت ناهمزمان و Async برای تموم شدن یک future صبر میکنه، و این کار اجازه میده تا task ها و عملیات های دیگه اجرا بشن اگه در حال حاضر اون future قادر به ادامه پردازشش نیست.

برای مثال فرض کنید با سه تابع async fn داریم به اسم های learn_song، sing_song و dance:

async fn learn_song() -> Song { /* ... */ }
async fn sing_song(song: Song) { /* ... */ }
async fn dance() { /* ... */ }

یه راه برای صدا زدن این سه تابع اینه که به صورت تک تک بهشون برسیم و thread بلاک بشه تا اجرا بشن:

fn main() {
    let song = block_on(learn_song());
    block_on(sing_song(song));
    block_on(dance());
}

با این حال با از کل توان و سرعتمون تو این روشن استفاده نکردیم برای اینکه فقط یک کار رو در لحظه داریم انجام میدیم! مشخصه که ما اول باید یاد بگیریم چجوری آهنگ بخونیم learn_song که بعدش واقعا بتوینم بخونیمش sing_song، ولی این شدنیه که طی یادگرفتن و خوندن آهنگ همزمان برقصیم dance. برای این کار میتونیم دو تا تابع async fn مستقل بسازیم که به صورت همزمان اجرا بشن:

async fn learn_and_sing() {
    // اینجا اول صبر میکنیم آهنگ یاد گرفته بشه قبل از خوندنش
    // انیجا ما از "اویت" به جای "بلاک آن" استفاده میکنیم تا جلوگیری
    // بشه از بلاک شدن و مسدود شدن ترد اینجوری میتونیم همزمان با این کار
    // تابع رقصیدن هم اجرا کنیم
    let song = learn_song().await;
    sing_song(song).await;
}

async fn async_main() {
    let f1 = learn_and_sing();
    let f2 = dance();

    // ماکرو "جوین" مثل "اویت" هست. با استفاده از این ماکرو میشه
    // برای تموم شدن چندین "فیوچر" به صورت همزمان صبر کرد.
    // اگه به صورت موقت تابع اولی ترد رو بخواد مسدود کنه کار تابع بعدی یعنی رقصیدن انجام میشه.
    // و برعکسش هم میتونه اتفاق بیفته، یعنی اگه رقصیدن بخواد مسدود بشه اون یکی تابع اجرا میشه
    // و اگه جفتشون بلاک بشن این فانکشن کلا بلاک میشه و اجراکنندش صبر میکنه تموم بشه کارش
    futures::join!(f1, f2);
}

fn main() {
    block_on(async_main());
}

توی این مثال، یاد گرفتن آهنگ learn_song باید قبل از خوندنش sing_song انجام بشه، ولی هر دو کار یادگرفتن و خوندن میتونه همزمان با رقصیدن dance اتفاق بیفته. اگه ما داخل learn_and_sing به جای learn_song().await از block_on(learn_song()) استفاده کرده بودیم، توی thread هیچ کاره دیگه ای نمیشد انجام بدیم وقتی learn_song اجرا میشد. که همین باعث میشد نتونیم همزمان با اون کارها برقصیم dance. با استفاده از .await روی learn_song که از نوع future بود، این اجازه رو به thread دادیم که به کار بقیه task ها و عملیات برسه اگه learn_song بلاک شده بود. با این روش این امکان فراهم شد تا چندین future به صورت همزمان توی یک thread کارشون تموم شه و به پایان برسن.