Перейти к содержанию

Простой пример асинхронного программирования в Rust

Posted on:15 апреля 2023 г. at 16:43

Example Dynamic OG Image link В этой статье блога я буду изучать библиотеку tokio и приводить примеры асинхронного программирования, которые покажут вам полезность асинхронного программирования.

Допустим, мы - ресторан, и нам нужно приготовить какой-нибудь завтрак. Но для того, чтобы сделать это, мы должны предоставить меню. Наше меню будет состоять из яиц, бекона, картофельных оладий, кофе, тостов, фасоли и вафель. Давайте предположим, что на данный момент в этом ресторане работает только 1 работник и одновременно он обслуживает только 1 клиента.

Во-первых, нам нужно создать функции, которые создают эти варианты завтрака.

async fn eggs() -> String {
    println!("Давайте начнем готовить яйца!");
    sleep(Duration::from_millis(1000)).await;
    println!("Яйца приготовлены!");
    "Яйца!".to_string()
}

async fn hasbrowns() -> String {
    println!("Давайте начнем готовить несколько тостов!");
    sleep(Duration::from_millis(2000)).await;
    println!("Тосты приготовлены!");
    "Тосты!".to_string()
}

Это два примера. Теперь давайте создадим нашего исполнителя с помощью tokio и запустим эти функции!

fn run_restaurant() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();

    let mut egg = " ".to_string();
    let mut hasbrown = " ".to_string();
    use std::time::Instant;
    let now = Instant::now();
    rt.block_on(
        async {
            let eggtask = eggs();
            let hashbrowntasks = hashbrowns();
            egg = eggtask.await;
            hasbrown = hashbrowntasks.await;
            //
        }
    );
    let elapsed = now.elapsed();
    println!("Прошло времени: {:.2?}", elapsed);
}

Ожидаемое затраченное время выполнения должно составлять 2000 миллисекунд, однако в результате 3000 миллисекунд! Вывод в терминале демонстрирует:

Давайте начнем готовить яйца!
Яйца приготовлены!
Давайте начнем готовить несколько тостов!
Тосты приготовлены!
Прошло времени: 3.00s

Эти задачи были выполнены не так, как мы ожидали, верно? Что мы хотели, так это то, что если мы запустим eggs(), то вместо ожидания мы перейдем к следующей задаче hashbrowns(), которая выполняется сразу же. Тут произойдет следующее: eggs() и hashbrowns() будут готовится поочереди! Однако, если мы используем futures::join! то это будет выполнено как мы задумали.

Давайте начнем готовить яйца!
Яйца приготовлены!
Давайте начнем готовить несколько тостов!
Тосты приготовлены!
Прошло времени: 2.00s

Таким образом, наш код в итоге будет выглядеть следующим образом в потоках:

fn run_restaurant() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();

    let mut egg = " ".to_string();
    let mut hasbrown = " ".to_string();
    use std::time::Instant;
    let now = Instant::now();
    rt.block_on(
        async {
            let eggtask = eggs();
            let hashbrowntasks = hashbrowns();
            (egg, hasbrown) = futures::join!(eggtask, hashbrowntasks);
            //egg = eggtask.await;
            //hasbrown = hashbrowntasks.await;
            //
        }
    );
    let elapsed = now.elapsed();
    println!("Прошло времени: {:.2?}", elapsed);
}

futures::join!, по сути, “Опрашивает несколько фьючерсов одновременно, возвращая кортеж всех результатов после завершения”, эти задачи должны выполняться одновременно. Мы также можем выполнять эти задачи асинхронно, например, использовать tokio::spawn.

fn run_restaurant() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();

    let mut egg = " ".to_string();
    let mut hasbrown = " ".to_string();
    use std::time::Instant;
    let now = Instant::now();
    rt.block_on(
        async {
            let eggtask = eggs();
            let hashbrowntasks = hashbrowns();

            // Давайте начнем готовить яица
            let egghandle = tokio::spawn(eggtask);
            // Давайте начнем готовить тосты
            let hashbrownhandle = tokio::spawn(hashbrowntasks);

            hasbrown = hashbrownhandle.await.unwrap();
            egg = egghandle.await.unwrap();
            //egg = eggtask.await;
            //hasbrown = hashbrowntasks.await;
            //
        }
    );
    let elapsed = now.elapsed();
    println!("Прошло времени: {:.2?}", elapsed);
}

Получаем результат:

Давайте начнем готовить яйца!
Яйца приготовлены!
Давайте начнем готовить несколько тостов!
Тосты приготовлены!
Прошло времени: 2.00s

Обратите внимание, как это, по сути, работает за один и тот же промежуток времени одновременно и параллельно. Это сравнение инкапсулирует полезность асинхронного программирования, мы получаем точно такой же результат за один и тот же промежуток времени между 2 потоками, работающими параллельно для их соответствующих задач, и 1 потоком, работающим одновременно между 2 задачами, которые по существу ограничены вводом-выводом.