| name | rust-async-runtime |
| description | Rust 非同步執行時技能。涵蓋 tokio 1.44 runtime 架構與選擇、async/await 語法模式、 Future trait 原理、常見陷阱(blocking in async、async trait)、 channel 通訊(mpsc/oneshot/broadcast/watch)、task spawn 策略、 select! 多路複用、graceful shutdown、async stream、Mutex 在 async 中的正確用法。 觸發關鍵詞:async, await, tokio, async-std, Future, spawn, channel, mpsc, select, runtime, blocking, async trait, stream, executor
|
Rust Async Runtime
適用場景
- 需要高併發 I/O(HTTP 伺服器、資料庫連線、WebSocket)
- 實作非同步任務排程、背景工作、定時器
- 使用 channel 進行任務間通訊
- 處理 async stream(串流資料處理、SSE、gRPC streaming)
- 需要 graceful shutdown 的長駐服務
- 在 async 環境中正確使用鎖與同步原語
核心知識
Runtime 選擇
| Runtime | 特性 | 適用場景 |
|---|
| tokio | 最成熟、生態系最大、work-stealing 排程器 | 生產環境首選,絕大多數專案 |
| async-std | API 貼近 std、較輕量 | 學習用途、小型專案 |
| smol | 極簡、可組合、無 macro | 嵌入式或需要最小依賴的場景 |
結論:除非有明確理由,一律選 tokio。生態系中 95%+ 的 async crate 以 tokio 為主要支援目標。
tokio Runtime 類型
#[tokio::main]
async fn main() { }
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async { })
}
#[tokio::main(flavor = "current_thread")]
async fn main() { }
選擇依據:
multi_thread:CPU 核心 > 1、需要平行處理、生產環境
current_thread:單元測試、嵌入式環境、確定性排程需求
async/await 基本原理
async fn 回傳一個實作 Future trait 的匿名型別
Future 是惰性的——宣告不會執行,必須被 .await 或交給 executor
.await 讓出控制權給 runtime,直到 Future 完成
- Runtime(executor)負責輪詢(poll)所有 Future 直到
Poll::Ready
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
blocking in async 問題
絕對不要在 async 上下文中執行同步阻塞操作:
async fn bad() {
std::thread::sleep(Duration::from_secs(5));
std::fs::read_to_string("large.txt").unwrap();
}
async fn good() {
tokio::time::sleep(Duration::from_secs(5)).await;
tokio::fs::read_to_string("large.txt").await.unwrap();
}
async fn also_good() {
let result = tokio::task::spawn_blocking(|| {
heavy_cpu_computation()
}).await.unwrap();
}
Channel 通訊模式
| Channel 類型 | 模式 | 用途 |
|---|
mpsc | 多生產者 → 單消費者 | 任務佇列、事件匯集 |
oneshot | 單次發送 → 單次接收 | 請求-回應、任務結果回傳 |
broadcast | 單生產者 → 多消費者 | 事件廣播、配置更新通知 |
watch | 單生產者 → 多消費者(僅最新值) | 狀態監控、設定熱更新 |
Task Spawn 策略
let handle = tokio::spawn(async move {
do_work().await
});
let result = handle.await?;
let result = tokio::task::spawn_blocking(move || {
compute_heavy_stuff()
}).await?;
let local = tokio::task::LocalSet::new();
local.run_until(async {
tokio::task::spawn_local(async {
}).await.unwrap();
}).await;
select! 多路複用
use tokio::select;
select! {
val = async_operation_a() => {
println!("A 先完成: {val}");
}
val = async_operation_b() => {
println!("B 先完成: {val}");
}
else => {
println!("所有分支都不可用");
}
}
注意:被取消的分支對應的 Future 會被 drop,確保你的 Future 是取消安全的(cancel-safe)。
mpsc::Receiver::recv() 是取消安全的,但 read_exact() 不是。
async Mutex 正確用法
let guard = std_mutex.lock().unwrap();
some_async_op().await;
drop(guard);
let data = {
let guard = std_mutex.lock().unwrap();
guard.clone()
};
some_async_op().await;
let guard = tokio_mutex.lock().await;
some_async_op().await;
drop(guard);
經驗法則:不跨 .await 用 std::sync::Mutex(更快);跨 .await 用 tokio::sync::Mutex。
程式碼範例
Basic: async/await + tokio::spawn + JoinSet
use std::time::Duration;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
tokio::time::sleep(Duration::from_millis(100)).await;
42
});
let result = handle.await.expect("任務 panic");
println!("單一任務結果: {result}");
let mut set = JoinSet::new();
for i in 0..5 {
set.spawn(async move {
tokio::time::sleep(Duration::from_millis(50 * i)).await;
i * 10
});
}
while let Some(res) = set.join_next().await {
println!("任務完成: {}", res.expect("任務 panic"));
}
}
Intermediate: channel 通訊 + select!
use tokio::sync::{mpsc, oneshot};
use tokio::select;
struct Request {
payload: String,
respond_to: oneshot::Sender<String>,
}
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<Request>(32);
let worker = tokio::spawn(async move {
while let Some(req) = rx.recv().await {
let response = format!("已處理: {}", req.payload);
let _ = req.respond_to.send(response);
}
});
for i in 0..3 {
let (resp_tx, resp_rx) = oneshot::channel();
tx.send(Request {
payload: format!("任務-{i}"),
respond_to: resp_tx,
}).await.expect("channel 已關閉");
select! {
result = resp_rx => {
println!("{}", result.expect("工作者已斷線"));
}
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
println!("請求 {i} 超時");
}
}
}
drop(tx);
worker.await.expect("工作者 panic");
}
Advanced: graceful shutdown + async stream + semaphore
use std::time::Duration;
use tokio::sync::{mpsc, Semaphore};
use tokio::signal;
use tokio_stream::wrappers::ReceiverStream;
use tokio_stream::StreamExt;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
let semaphore = Arc::new(Semaphore::new(3));
let (data_tx, data_rx) = mpsc::channel(64);
let producer = tokio::spawn(async move {
for i in 0u64.. {
if data_tx.send(i).await.is_err() { break; }
tokio::time::sleep(Duration::from_millis(100)).await;
}
});
let sem = semaphore.clone();
let consumer = tokio::spawn(async move {
let mut stream = ReceiverStream::new(data_rx)
.map(|n| n * 2)
.take(20);
while let Some(value) = stream.next().await {
let permit = sem.clone().acquire_owned().await.expect("semaphore 已關閉");
tokio::spawn(async move {
println!("處理: {value}");
tokio::time::sleep(Duration::from_millis(200)).await;
drop(permit);
});
}
});
tokio::select! {
_ = signal::ctrl_c() => {
println!("\n收到 Ctrl+C,開始優雅關閉...");
}
_ = consumer => {
println!("消費者處理完畢");
}
}
drop(shutdown_tx);
producer.abort();
let _ = semaphore.acquire_many(3).await;
println!("所有任務已完成,程式結束");
}
常見錯誤對照表
| 錯誤訊息 | 原因 | 修復方式 |
|---|
future cannot be sent between threads safely / X is not Send | tokio::spawn 要求 Future 為 Send,但持有了非 Send 型別(如 Rc、MutexGuard) | 改用 Arc 取代 Rc;縮小 MutexGuard 作用域使其不跨 .await;或使用 spawn_local |
Cannot start a runtime from within a runtime | 在 async 區塊內呼叫 block_on() 導致巢狀 runtime | 移除內層 block_on(),改用 .await;若整合同步庫,用 spawn_blocking + 獨立 runtime |
blocking annotated I/O must be called from the context of the Tokio runtime | 在 runtime 外部呼叫 tokio I/O 函式 | 確保程式碼在 #[tokio::main] 或 Runtime::block_on 範圍內執行 |
the trait bound ... async_trait is not satisfied / async fn in trait 問題 | Rust edition < 2024 不支援原生 async fn in trait,或物件安全限制 | Edition 2024 原生支援 async fn in trait;需要 dyn Trait 時使用 #[trait_variant::make(SendTrait: Send)] 或 async-trait crate |
| 死鎖:程式卡住不動 | 持有 std::sync::Mutex 鎖跨 .await 點 | 縮小臨界區避免跨 .await,或改用 tokio::sync::Mutex |
capacity of channel is full / 發送端卡住 | mpsc channel 已滿,背壓未處理 | 增大 buffer、加速消費端、用 try_send 非阻塞處理、或使用 unbounded_channel(小心 OOM) |
JoinError: task panicked | tokio::spawn 的任務發生 panic | 在 spawn 的 async 區塊內加 catch_unwind,或在 .await 時用 match 處理 JoinError |
Cargo.toml 依賴模板
[package]
name = "my-async-app"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.44", features = ["full"] }
tokio-stream = "0.1"
tokio-util = "0.7"
futures = "0.3"
async-trait = "0.1"
pin-project-lite = "0.2"
reqwest = { version = "0.12", features = ["json"] }
tower = "0.5"
tracing = "0.1"
tracing-subscriber = "0.3"
[dev-dependencies]
tokio = { version = "1.44", features = ["full", "test-util"] }
注意:生產環境可只啟用所需 feature(如 rt-multi-thread, macros, net, time)以減少編譯時間。
參考來源
- Tokio Tutorial — 官方教學,涵蓋 spawning、channels、I/O
- Asynchronous Programming in Rust — Rust 官方 async book
- Tokio API Documentation — tokio 1.x 完整 API 參考
- Alice Ryhl — Actors with Tokio — channel-based actor 模式實戰
- Jon Gjengset — Decrusting the tokio crate — 深入 tokio 內部架構
- Rust Edition 2024 — async fn in trait — 原生 async trait 支援說明