Rust async/Future 与 Send/Sync
更新时间:2026-09-18。Rust 的异步是「语言提供 Future + 外部运行时驱动」的组合,和内置 runtime 的语言差别很大。深入版见 异步运行时内部、错误处理与异步。
一、Future 是惰性的状态机
async fn 不会在调用时执行,而是返回一个实现了 Future 的状态机;直到被运行时 poll(通过 .await 或 spawn)才推进:
async fn fetch(id: u64) -> String { /* 遇到 IO 就挂起,不占线程 */ }
#[tokio::main]
async fn main() {
let s = fetch(1).await; // .await:挂起当前任务、让出线程,就绪后恢复
tokio::spawn(async { fetch(2).await; }); // 交给运行时并发调度
}.await 的本质是「若没就绪就返回 Pending、让出执行权,就绪后从上次状态恢复」,因此等待期间不阻塞 OS 线程,少量线程可承接到大量并发任务。
二、Send / Sync
| trait | 含义 |
|---|---|
T: Send | T 的所有权可以安全转移到另一个线程 |
T: Sync | &T 可以安全地被多个线程共享(等价 &T: Send) |
大多数类型由编译器自动派生(i32、String 都是 Send+Sync);Rc、裸指针、RefCell 不是 Send/Sync。需要手动实现时要写 unsafe impl,这是在向编译器「担保」线程安全。
三、为什么跨 .await 要 Send
tokio::spawn 要求 Future: Send + 'static,因为任务可能在一个线程挂起、被调度到另一个线程恢复,跨越挂起点仍存活的变量都会被编进状态机:
// 编译错误:std::sync::MutexGuard 不是 Send,跨过 .await 仍被持有
let guard = std_lock.lock().unwrap();
some_async().await; // guard 在此存活 → future 不是 Send
drop(guard);修法:把 guard 的作用域限制在 .await 之前结束(用块或先 drop),异步场景优先用 tokio::sync::Mutex。
四、常见坑
- 忘了驱动 Future:只调用
async fn不.await/不 spawn,代码根本不执行(还会有 unused 警告)。 - 在 async 里跑阻塞代码:
std::thread::sleep、大 CPU 计算、同步文件 IO 会堵死整个 worker 线程,应用tokio::time::sleep,阻塞活放spawn_blocking。 - 在非 async 上下文
.await:.await只能在 async fn/block 内;同步入口用block_on/runtime。 'static误解:spawn 要求'static是说任务不能持有可能提前失效的栈引用,用所有权或Arc,不是硬标生命周期。- 并发误用:要同时跑多个 Future 用
tokio::join!/select!,连续.await仍是串行。
总结
Rust 的 async fn 返回惰性 Future(编译器生成的状态机),调用本身不执行,靠 tokio 等外部运行时 poll、用 .await 在未就绪时让出线程、就绪后恢复,因此等待不阻塞线程;Send 表示所有权可跨线程转移、Sync 表示引用可跨线程共享,大多自动派生而 Rc/RefCell/裸指针不满足;tokio::spawn 要求 Future 为 Send+'static,使任何跨过 .await 仍存活的非 Send 变量(如 std 锁守卫)都导致编译失败,要在 await 前释放或改用异步锁,同时切忌在异步任务里直接执行阻塞/重 CPU 代码,应交给 spawn_blocking。