[工作經驗] Rust tokio 不想 await task 但卻想提早知道有沒有 error

假設我們想要平行地跑兩個 tasks A 和 B,我們用 async task A 來跑主要的程式,用 task B 來監控正在跑的程式。Task A 跑完了我們就不管 task B 可以直接結束程式了,所以 task B 我們並不想用 await 去等待他。

這時候就有一個小陷阱正在等著我們:如果 task B 提早發生錯誤,我們可能就會忘記去處理他。

壞程式

最近就不小心寫出像下面這樣的 code:


use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let task_a_handle = tokio::spawn(async move {
        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;

        if true {
            anyhow::bail!("Something went wrong in task A");
        } else {
            Ok(())
        }
    });

    tokio::task::spawn_blocking(move || {
        std::thread::sleep(std::time::Duration::from_secs(1));

        if true {
            anyhow::bail!("Something went wrong in task B");
        } else {
            Ok(())
        }
    });

    match task_a_handle.await? {
        Ok(_) => println!("Finished"),
        Err(e) => panic!("Something wrong: {e}"),
    }

    Ok(())
}

我們可以看到 task B 由於可能要呼叫一些 blocking API 去監控程式,可能會用 loop + sleep 每隔幾秒會 block 一陣子,但如果 task A 一結束執行我們可能會想要趕快做別的事。畢竟程式一結束之後我們可能關心的是最終的結果,我們就沒必要還要等監控的 task B 跑完。

這邊我用 anyhow crate 來讓 error propagation 變得比較輕鬆一點,可以把程式碼丟到 VSCode 用 rust-analyzer 看一下 handles 的 types 就能知道只是有點過度包裝的感覺。

但是萬一 task B 在 task A 還沒跑完的時候就遇到錯誤壞掉了怎辦? 這邊我們讓 task A 在 5 秒後遇上錯誤,task B 則是在 1 秒後遇上錯誤來模擬這件事情。


如果我們把上面程式丟到 Rust Playgrond 去執行,我們會得到下面的結果:


thread 'main' panicked at 'Something wrong: Something went wrong in task A', src/main.rs:38:19


Oops, 監控程式跑到一半壞掉了我們都不知道。但是想要得到 Result 好像就非得要 await 他不可,但我們就是不想 await 他怎麼辦?

解法

後來發現,我們可以用 tokio::select 來做到這件事,邏輯會變成是,我們等待 either task A or B 完成,誰先完成我們就去看他的 Result:


use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let task_a_handle = tokio::spawn(async move {
        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;

        if true {
            anyhow::bail!("Something went wrong in task A");
        } else {
            Ok(())
        }
    });

    let task_b_handle = tokio::task::spawn_blocking(move || {
        std::thread::sleep(std::time::Duration::from_secs(1));

        if true {
            anyhow::bail!("Something went wrong in task B");
        } else {
            Ok(())
        }
    });

    let wait_handle = tokio::spawn(async move {
        tokio::select! {
            result = task_a_handle => {
                result?
            }
            result = task_b_handle => {
                result?
            }
        }
    });

    match wait_handle.await? {
        Ok(_) => println!("Finished"),
        Err(e) => panic!("Something wrong: {e}"),
    }

    Ok(())
}


執行後,我們終於可以提前知道 task B 發生錯誤:


thread 'main' panicked at 'Something wrong: Something went wrong in task B', src/main.rs:38:19

Best Practice

或許我們可以得出一個結論,就是不管怎麼樣,我們應該都要想辦法解讀 task handle 完成的結果,千萬別放牛吃草,spawn 下去就不管他。

留言

此網誌的熱門文章

[試算表] 追蹤台股 Google Spreadsheet (未實現損益/已實現損益)

[Side Project] 互動式教學神經網路反向傳播 Interactive Computational Graph

[插件] 在 Chrome 網頁做區分大小寫的搜尋