Rust 异步编程
`async`/`await`、`Future`、`tokio`、以及并发与并行的区别。
基本形态
[dependencies]
tokio = { version = "1", features = ["full"] }async fn fetch_data() -> String {
println!("开始");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
"数据".to_string()
}
#[tokio::main]
async fn main() {
let data = fetch_data().await;
println!("{}", data);
}开始
数据#[tokio::main] wraps async fn main in a synchronous main that starts the tokio runtime. Without a runtime, an async function does nothing — it only builds a Future.
并发 vs 并行
use std::time::Instant;
async fn task(name: &str, ms: u64) -> String {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
format!("{} 完成", name)
}
#[tokio::main]
async fn main() {
// 串行:总耗时约 300ms
let t = Instant::now();
let a = task("a", 100).await;
let b = task("b", 100).await;
let c = task("c", 100).await;
println!("{} {} {} 串行 {:?}", a, b, c, t.elapsed());
// 并发:总耗时约 100ms
let t = Instant::now();
let (a, b, c) = tokio::join!(
task("a", 100),
task("b", 100),
task("c", 100)
);
println!("{} {} {} 并发 {:?}", a, b, c, t.elapsed());
}a 完成 b 完成 c 完成 串行 301ms
a 完成 b 完成 c 完成 并发 101msjoin! drives three futures concurrently (interleaved on one thread); spawn adds multi-threaded parallelism. That is the difference: async is waiting without blocking, not using more cores.
spawn 并行
#[tokio::main]
async fn main() {
let h1 = tokio::spawn(async { 1 + 1 });
let h2 = tokio::spawn(async { 2 + 2 });
let (a, b) = (h1.await.unwrap(), h2.await.unwrap());
println!("{} {}", a, b);
}2 4tokio::spawn requires a 'static + Send future, so borrowing locals is not allowed — move or clone owned values in. That is why async code clones more.Future 是什么
// Future 就是一个可以被反复轮询的状态机
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}async fn is compiled into a state machine implementing Future: each .await is a suspension point. Pending means "not ready, yield the thread"; Ready means done.
常见组合子
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// join!:等全部完成
let (a, b) = tokio::join!(sleep(Duration::from_millis(10)), async { 42 });
let _ = a;
println!("{}", b);
// try_join!:任一失败就返回
// select!:谁先完成用谁
let mut t1 = Box::pin(sleep(Duration::from_millis(50)));
let mut t2 = Box::pin(sleep(Duration::from_millis(20)));
tokio::select! {
_ = &mut t1 => println!("t1 先"),
_ = &mut t2 => println!("t2 先"),
}
}42
t2 先| Macro | Behaviour |
|---|---|
join! | 等全部完成 |
try_join! | 等全部完成,遇错即返回 |
select! | 第一个完成的胜出,其余取消 |
spawn | 丢到运行时并行跑 |
超时与取消
use tokio::time::{timeout, Duration};
async fn slow() -> &'static str {
tokio::time::sleep(Duration::from_secs(10)).await;
"完成"
}
#[tokio::main]
async fn main() {
match timeout(Duration::from_millis(100), slow()).await {
Ok(v) => println!("{}", v),
Err(_) => println!("超时了"),
}
}超时了A timeout cancels the future by dropping it. This is where Rust async shines: cancellation needs no API — dropping is cancellation.
std::fs::read, std::thread::sleep, CPU-heavy work) — they stall the whole executor thread. Use tokio::fs, tokio::time::sleep, and spawn_blocking for CPU work.基本形态
[dependencies]
tokio = { version = "1", features = ["full"] }async fn fetch_data() -> String {
println!("开始");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
"数据".to_string()
}
#[tokio::main]
async fn main() {
let data = fetch_data().await;
println!("{}", data);
}开始
数据#[tokio::main] 把 async fn main 包成一个同步 main,内部启动 tokio 运行时。没有运行时的话,async 函数什么都不会发生——它只是构造了一个 Future。
并发 vs 并行
use std::time::Instant;
async fn task(name: &str, ms: u64) -> String {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
format!("{} 完成", name)
}
#[tokio::main]
async fn main() {
// 串行:总耗时约 300ms
let t = Instant::now();
let a = task("a", 100).await;
let b = task("b", 100).await;
let c = task("c", 100).await;
println!("{} {} {} 串行 {:?}", a, b, c, t.elapsed());
// 并发:总耗时约 100ms
let t = Instant::now();
let (a, b, c) = tokio::join!(
task("a", 100),
task("b", 100),
task("c", 100)
);
println!("{} {} {} 并发 {:?}", a, b, c, t.elapsed());
}a 完成 b 完成 c 完成 串行 301ms
a 完成 b 完成 c 完成 并发 101msjoin! 让三个 future 并发推进(单线程内交替),spawn 才会用到多线程并行。这是异步和并发的核心区别:异步是「不阻塞地等待」,不是「同时用多个核」。
spawn 并行
#[tokio::main]
async fn main() {
let h1 = tokio::spawn(async { 1 + 1 });
let h2 = tokio::spawn(async { 2 + 2 });
let (a, b) = (h1.await.unwrap(), h2.await.unwrap());
println!("{} {}", a, b);
}2 4tokio::spawn 要求 future 是 'static + Send,所以不能借用局部变量——要 move 或 clone 成拥有所有权的值。这是异步代码里 clone 变多的根本原因。Future 是什么
// Future 就是一个可以被反复轮询的状态机
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}async fn 被编译器改写成实现了 Future 的状态机:每个 .await 是一个可能的暂停点。Pending 表示「还没好,先让出线程」,Ready 表示「完成了」。
常见组合子
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// join!:等全部完成
let (a, b) = tokio::join!(sleep(Duration::from_millis(10)), async { 42 });
let _ = a;
println!("{}", b);
// try_join!:任一失败就返回
// select!:谁先完成用谁
let mut t1 = Box::pin(sleep(Duration::from_millis(50)));
let mut t2 = Box::pin(sleep(Duration::from_millis(20)));
tokio::select! {
_ = &mut t1 => println!("t1 先"),
_ = &mut t2 => println!("t2 先"),
}
}42
t2 先| 宏 | 行为 |
|---|---|
join! | 等全部完成 |
try_join! | 等全部完成,遇错即返回 |
select! | 第一个完成的胜出,其余取消 |
spawn | 丢到运行时并行跑 |
超时与取消
use tokio::time::{timeout, Duration};
async fn slow() -> &'static str {
tokio::time::sleep(Duration::from_secs(10)).await;
"完成"
}
#[tokio::main]
async fn main() {
match timeout(Duration::from_millis(100), slow()).await {
Ok(v) => println!("{}", v),
Err(_) => println!("超时了"),
}
}超时了timeout 超时会取消那个 future(直接 drop 它)。这是 Rust 异步比很多语言优雅的地方:取消不需要额外的 API,drop 就是取消。
std::fs::read、std::thread::sleep、CPU 密集计算),它们会卡住整个 executor 线程。用 tokio::fs、tokio::time::sleep,CPU 密集任务丢 spawn_blocking。