Rust 異步程式設計完整入門指南
Meta Description
掌握 Rust async/await 與 tokio 的核心概念與實戰技巧:理解 Future、Executor 架構、避免 deadlock,並高效處理 HTTP client、DB query 与 concurrent tasks。適用於有基礎 Rust 知識的開發者。
什麼是 Rust 異步程式設計?為什麼需要它?
Rust 異步程式設計(asynchronous programming)是一種讓程式「非阻塞」執行 I/O 操作的方式。與傳統多線程(multi-threaded)相比,Rust async 能以單一執行緒高效處理大量 concurrent request(如 HTTP 请求、資料庫查詢),大幅降低資源消耗。
關鍵區別:
Thread:操作系统預先分配執行時間,每個 thread 需占用記憶體與 CPU context switch 成本Async:程式碼主動「yield」等待 I/O 完成,無需額外 thread;executor(如 tokio)负责调度 future
Rust 本身不提供 runtime,async 函數會回傳 Future —— 一種 lazy promise object(延遲承諾物件)。只有在 executor(執行器)中被 poll 時才實際執行。
核心概念:Future、Executor 與 tokio
Future 是什麼?
Future 是 Rust 中的异步计算 unit(計算單元),實作 std::future::Future trait:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>)
-> Poll<Self::Output>;
}
Poll:Ready(T)或Pending(尚未完成)Pin:確保 future 不被移動,防止自引用失效
注意:呼叫 async 函數不會立即執行!它只建立 Future object。
Executor 的角色
Executor 負責 poll future 至完成:
- tokio runtime:最主流 executor,提供多種 scheduler(single-thread、multi-thread、work-stealing)
spawnvsawait:spawn(future):非阻塞啟動,立即返回 handleawait future:阻塞當前 task 直至完成
tokio 簡介
tokio 是 Rust 最廣泛使用的 async runtime。核心功能包括:
tokio::spawn():啟動新 tasktokio::time::sleep():非阻塞 delaytokio::net::TcpStream、HttpClient:async I/O API
安装方式(Cargo.toml):
[dependencies]
tokio = { version = "1.37", features = ["full"] }
實戰範例 1:HTTP Client with tokio
以下示範使用 reqwest + tokio 發送非阻塞 HTTP request:
use tokio;
use reqwest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
// 並行發送 3 requests(非 sequential)
let futures = vec![
client.get("https://api.github.com/repos/rust-lang/rust").send(),
client.get("https://api.github.com/repos/tokio-rs/tokio").send(),
client.get("https://api.github.com/repos/actix/actix-web").send()
];
// 使用 join! 并行等待所有 futures
let responses = futures::future::join_all(futures).await;
for (i, res) in responses.iter().enumerate() {
println!("Request {} status: {}", i+1, res?.status());
}
Ok(())
}
關鍵技巧:
join_all:parallel await multiple futurestokio::main:自動初始化 tokio runtime
實戰範例 2:Concurrent Tasks with spawn
當需要「fire and forget」或 parallel tasks,使用 spawn:
use tokio;
#[tokio::main]
async fn main() {
// 启動独立 task(不 await)
let handle1 = tokio::spawn(async {
println!("Task 1 starts");
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
"Task 1 result"
});
let handle2 = tokio::spawn(async {
println!("Task 2 starts");
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
"Task 2 result"
});
// await results(非 sequential order)
let res1 = handle1.await.unwrap();
let res2 = handle2.await.unwrap();
println!("Results: {} {}", res1, res2);
}
輸出順序:
Task 1 starts
Task 2 starts ← Task 2 先完成(1s < 2s)
Results: Task 1 result Task 2 result
實戰範例 3:Non-blocking Database Query
以 PostgreSQL 為例,使用 tokio-postgres:
use tokio_postgres;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (client, conn) = tokio_postgres::connect(
"host=localhost user=postgres password=1234 dbname=test",
tokio_postgres::NoTls
).await?;
// 啟動 connection task(獨立于 main task)
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("Connection error: {}", e);
}
});
// 非阻塞 query
let rows = client.query("SELECT * FROM users LIMIT 5", &[]).await?;
for row in rows {
println!("User ID: {} Name: {}",
row.get::<usize, i32>(0),
row.get::<usize, String>(1)
);
}
Ok(())
}
最佳實踐:
conn必须 spawn,避免 main task 被 blocking- 使用
try_join!/select!避免 deadlock(後述)
注意事项 1:!Send Types 與 Thread Safety
Rust async runtime 常要求 Send bound。若 object not Send(如 Rc<T>、RefCell<T>),需特殊處理:
use std::rc::Rc;
// ❌ This won't compile: Rc not Send
#[tokio::main]
async fn main() {
let data = Rc::new("Hello");
tokio::spawn(async move {
println!("{}", data); // Error: Rc<T> is !Send
});
}
解決方案:
- 改用
Arc<Mutex<T>>(thread-safe) - 在 single-thread runtime 中 spawn non-Send task:
tokio::spawn(async move { ... }) // with "rt-single-thread" feature
注意事项 2:Spawn vs Await 的陷阱
Deadlock 预防:
// ❌ Deadlock risk:await inside spawn
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
await_other_task().await; // 若 other task await main → deadlock
});
handle.await;
}
安全寫法:
- 使用
join!或try_join!(parallel without blocking) - 避免 circular dependency(如 A await B, B await A)
use futures::future::join;
let (res1, res2) = join(future1(), future2()).await;
注意事项 3:Context 與 Pinning
當實作 custom Future,需注意 Pin:
use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};
struct MyFuture {
state: String,
}
impl Future for MyFuture {
type Output = String;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
// 使用 self.get_mut() 安全修改
let this = self.get_mut();
this.state = "completed";
Poll::Ready(this.state.clone())
}
}
注意:
- 不可直接
*self或std::mem::replace - 必須使用
Pin::get_mut()或 unsafeUnpinbound
Debug 技巧與推薦工具
1. Runtime tracing with tokio-console
# 启动 debug console(需 nightly)
cargo +nightly install tokio-console
RUSTFLAGS="-Ctarget-cpu=native" cargo run --features tokio/tracing
2. Future visualization:futures-instrument
use futures::instrument;
#[instrument(level = "debug", skip_all)]
async fn complex_task() {
// 自動 trace execution path
}
3. 常見 error 解析:
task panicked:spawn task 内部 error → 使用catch_unwindno runtime registered:缺少#[tokio::main]或手动Builder::new_multi_thread()
推薦學習資源
| Resource | Type | Link |
|---|---|---|
| The Rust Async Book | Official Guide | https://rust-lang.github.io/async-book/ |
| Tokio Tutorial | Hands-on | https://tokio.rs/tokio/tutorial |
| Futures Explained | API Reference | docs.rs/fut... |