[工作經驗] 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 下去就不管他。
留言
發佈留言