Skip to content

陷阱与选型

async 的高频坑(阻塞、取消安全)与线程/async 的选型判断。

常见错误与陷阱

async 块捕获引用导致 'static 报错

rust
// 依赖:tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let name = String::from("worker");
    // tokio::spawn(async { println!("{name}"); });
}
text
error[E0373]: async block may outlive the current function, but it borrows `name`,
              which is owned by the current function
 --> src/main.rs:4:22
  |
4 |     tokio::spawn(async { println!("{name}"); });
  |                      ^^^^^          ---- `name` is borrowed here
  |                      |
  |                      may outlive borrowed value `name`
  |
note: async block is returned here
help: to force the async block to take ownership of `name` (and any other
      variables referenced), you can use the `move` keyword
  |
4 |     tokio::spawn(async move { println!("{name}"); });
  |                      ++++

修法:

  • move:把所有权交给任务(最常见的修法)。
  • Arc:多个任务需要共享同一份数据时。
  • tokio::task::scope尚未稳定,nightly)或 join!:确需借用时, 让任务与作用域同生命周期。

.awaitMutexGuardfuture cannot be sent between threads safely

rust
use std::sync::Mutex;

async fn bad(lock: &Mutex<u32>) -> u32 {
    let guard = lock.lock().unwrap();
    yield_once().await;      // 守卫跨过了这个挂起点
    *guard
}

async fn yield_once() {}

fn assert_send<T: Send>(_t: T) {}

fn main() {
    let m = Mutex::new(1u32);
    assert_send(bad(&m));
}

rustc 1.98.1 的完整错误(这是本章最值得背下来的错误之一):

text
error: future cannot be sent between threads safely
  --> src/main.rs:16:17
   |
16 |     assert_send(bad(&m));
   |                 ^^^^^^^ future returned by `bad` is not `Send`
   |
   = help: within `impl Future<Output = u32>`, the trait `Send` is not implemented
     for `std::sync::MutexGuard<'_, u32>`
note: future is not `Send` as this value is used across an await
  --> src/main.rs:6:18
   |
 5 |     let guard = lock.lock().unwrap();
   |         ----- has type `std::sync::MutexGuard<'_, u32>` which is not `Send`
 6 |     yield_once().await;
   |                  ^^^^^ await occurs here, with `guard` maybe used later
   |
note: required by a bound in `assert_send`

在真实项目里,这个诊断出现在 tokio::spawn 的调用点:

text
error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
   |
   = help: within `{async block@src/main.rs:5:18}`, the trait `Send` is not
     implemented for `std::sync::MutexGuard<'_, u32>`
note: future is not `Send` as this value is used across an await
note: required by a bound in `tokio::spawn`
   |
   = note: required because it appears within the type `{async block@...}`

读法

  1. 第一行告诉你哪个 futureSend
  2. help 行告诉你哪个字段(这里是 MutexGuard<'_, u32>)不 Send
  3. note: ... used across an await 精确指到挂起点,并标出这个值是什么时候创建的。

修法:把守卫的作用域缩到 .await 之前(首选),或换 tokio::sync::Mutex(见「tokio::syncstd 对照」)。

在 async 里做阻塞操作会卡住整个 runtime

rust
// 依赖:tokio = { version = "1", features = ["full"] }
use std::time::Duration;
use tokio::task::yield_now;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    tokio::spawn(async {
        // 错:std::thread::sleep 会占住唯一的 worker 线程,
        // 下面那个本该 10ms 打印的任务会被推迟到 3 秒之后
        std::thread::sleep(Duration::from_secs(3));
    });

    tokio::spawn(async {
        tokio::time::sleep(Duration::from_millis(10)).await;
        println!("this should print at ~10ms, but current_thread is blocked");
    });

    tokio::time::sleep(Duration::from_secs(4)).await;
    let _ = yield_now().await;
}

正确写法:

rust
// 依赖:tokio = { version = "1", features = ["full"] }
use std::time::Duration;

// 阻塞 IO / CPU 密集 → spawn_blocking
async fn hash_file(path: &str) -> std::io::Result<u64> {
    let path = path.to_owned();
    tokio::task::spawn_blocking(move || {
        let data = std::fs::read(path)?;
        // 假装这里是昂贵的哈希计算
        Ok(data.iter().map(|b| u64::from(*b)).sum())
    })
    .await
    .expect("blocking task panicked")
}

// 纯等待 → tokio::time::sleep(让出线程)
async fn polite_wait() {
    tokio::time::sleep(Duration::from_millis(10)).await;
}

#[tokio::main]
async fn main() {
    let sum = hash_file("Cargo.toml").await.expect("read failed");
    println!("{sum}");
    polite_wait().await;
}

⚠️ 陷阱#[tokio::main] 默认是 multi_thread,一个任务阻塞只会占住一个 worker, 症状更隐蔽(表现为「负载上不去、延迟尾部长」),比 current_thread 下直接卡死更难发现。 发现 runtime 里出现 std::thread::sleep / std::fs::* / 同步 reqwest::blocking 就要警惕。

? 在 async 中正常工作,但错误类型要统一

? 只是 match + From::from 的语法糖,与 async 无关,因此 async fn 里可以正常用:

rust
// 依赖:tokio = { version = "1", features = ["full"] }
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
}

// 实现 From 后,? 会自动转换错误类型
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

async fn read_number(path: &str) -> Result<i32, AppError> {
    let text = tokio::fs::read_to_string(path).await?;  // io::Error -> AppError
    let n: i32 = text.trim().parse()?;                  // ParseIntError -> AppError
    Ok(n)
}

#[tokio::main]
async fn main() {
    // 练习/示例里用 expect 说明期望值;生产代码请把 AppError 往上抛
    match read_number("Cargo.toml").await {
        Ok(n) => println!("{n}"),
        Err(e) => println!("failed: {e:?}"),
    }
}

⚠️ 陷阱async 不会改变 ? 的规则,它只改变「函数体如何被编译成状态机」。 所以两件事要自己保证:

  1. async fn 的返回类型必须显式声明成 Result<T, E>;写成 async fn f() { ...? ... } 会在 ? 处报 error[E0308]: mismatched types — expected '()', found 'Result<_, _>'
  2. 多个 ? 必须能转成同一个 E,否则报 error[E0277]: '?' couldn't convert the error to 'E',需要手写 From (示例里那样)或用 thiserror(库)/anyhow(应用)简化样板。

另外 ?OptionResult 混用时依然不行——这条规则与 async 无关。

忘记 .await:future 根本没执行

rust
async fn fetch(url: &str) -> String {
    format!("GET {url}")
}

fn main() {
    fetch("https://example.com");    // 只是构造了一个 future,然后丢掉
}
text
warning: unused implementer of `Future` that must be used
 --> src/main.rs:8:5
  |
8 |     fetch("https://example.com");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: futures do nothing unless you `.await` or poll them
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
  • Future trait 本身带 #[must_use],所以「构造即丢弃」至少有一条警告。
  • let f = fetch(url); 之后 drop(f)静默的:变量被使用了,没有警告。 这类 bug 的表现是「请求没发出去 / 文件没写 / 计数不对」,排查时先搜 「是不是漏了 .await」。
  • clippylet_underscore_future 这一 lint 专门抓 let _ = async_fn();

递归 async 需要 Box::pin

rust
async fn countdown(n: u32) -> u32 {
    if n == 0 { 0 } else { 1 + countdown(n - 1).await }
}
text
error[E0733]: recursion in an async fn requires boxing
 --> src/main.rs:1:1
  |
1 | async fn countdown(n: u32) -> u32 {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
4 |         1 + countdown(n - 1).await
  |             ---------------------- recursive call here
  |
  = note: a recursive `async fn` call must introduce indirection such as
    `Box::pin` to avoid an infinitely sized future

原因:状态机类型的大小必须在编译期确定,而递归调用意味着「状态机里嵌一个自己」, 大小无穷。修法是引入一层间接(indirection):

rust
// 依赖:无(纯 std)
use std::future::Future;
use std::pin::Pin;

fn countdown(n: u32) -> Pin<Box<dyn Future<Output = u32> + Send>> {
    Box::pin(async move {
        if n == 0 {
            0
        } else {
            // 这里等待的是 Box::pin 出来的 future,大小固定为一个指针
            1 + countdown(n - 1).await
        }
    })
}

fn main() {
    fn assert_future<F: Future<Output = u32>>(_f: F) {}
    assert_future(countdown(5));
}

更贴近实战的递归形态(目录遍历、重试)通常改用显式栈或 futures::future::BoxFuture 别名:

rust
// 依赖:futures = "0.3"
use futures::future::BoxFuture;
use futures::FutureExt;   // 提供 .boxed()

fn walk(path: String, depth: u32) -> BoxFuture<'static, Vec<String>> {
    async move {
        let mut out = vec![path.clone()];
        if depth > 0 {
            // 递归展开,每次调用返回一个装箱的 future
            out.extend(walk(format!("{path}/sub"), depth - 1).await);
        }
        out
    }
    .boxed()   // 等价于 Box::pin 的友好写法
}

fn main() {
    let names = futures::executor::block_on(walk(String::from("root"), 2));
    println!("{names:?} {}", names.len());
}

输出:["root", "root/sub", "root/sub/sub"] 3

async fn 在 trait 中:1.75+ 能写,但 dyn 仍需变通

Rust 1.75 起,trait 里可以直接写 async fn(RPITIT 稳定),不再强制 #[async_trait]

rust
// 依赖:无(需要 rustc >= 1.75)
trait Store {
    async fn get(&self, key: &str) -> Option<String>;
    async fn put(&self, key: &str, value: String);
}

struct MemStore;

impl Store for MemStore {
    async fn get(&self, key: &str) -> Option<String> {
        Some(format!("value of {key}"))
    }

    async fn put(&self, _key: &str, _value: String) {}
}

async fn use_generic<S: Store>(s: &S) {
    // 泛型里可以直接调用,编译器为每个 S 生成专用状态机
    if let Some(v) = s.get("k").await {
        println!("{v}");
    }
}

fn main() {
    fn assert_future<F: std::future::Future>(_f: F) {}
    assert_future(use_generic(&MemStore));
}

限制与 workaround

需求1.75+ 直接写法说明
泛型 fn f<S: Store>(s: &S)✅ 可以直接 .await单态化,零开销
dyn Store❌ 编译错误async fn 在 trait 里返回匿名类型,大小未知,不能进 vtable
需要 dyn手写 -> Pin<Box<dyn Future<Output = T> + Send + '_>>,或在 impl 里 Box::pin多一次堆分配
只要省事#[async_trait] 宏(crate async-trait宏自动改写为 Pin<Box<dyn Future>>,代价同上

手写 desugar 的样子:

rust
// 依赖:无。为了让 trait 能当 dyn 用,手动把 async fn 写成返回装箱 future
use std::future::Future;
use std::pin::Pin;

trait Store {
    fn get<'a>(&'a self, key: &'a str) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>>;
}

struct MemStore;

impl Store for MemStore {
    fn get<'a>(
        &'a self,
        key: &'a str,
    ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
        Box::pin(async move { Some(format!("value of {key}")) })
    }
}

async fn use_dyn(store: &dyn Store) {
    // dyn Store 现在可以用了
    if let Some(v) = store.get("k").await {
        println!("{v}");
    }
}

fn main() {
    fn assert_future<F: Future>(_f: F) {}
    assert_future(use_dyn(&MemStore));
}

trait 里的 async fn 默认不保证 Send,所以把它放进 tokio::spawn 会报错:

rust
trait Store {
    async fn save(&self, key: &str);
}

async fn needs_send<S: Store + Sync>(s: &S) {
    s.save("k").await;
}

fn assert_send<T: Send>(_t: T) {}

async fn generic<S: Store + Sync>(s: &S) {
    assert_send(needs_send(s));
}
text
error: future cannot be sent between threads safely
   |
13 |     assert_send(needs_send(s));
   |                 ^^^^^^^^^^^^^ future returned by `needs_send` is not `Send`
   |
   = help: within `impl Future<Output = ()>`, the trait `Send` is not implemented
     for `impl Future<Output = ()>`
note: future is not `Send` as it awaits another future which is not `Send`
  --> src/main.rs:7:5
   |
 7 |     s.save("k").await;
   |     ^^^^^^^^^^^ await occurs here on type `impl Future<Output = ()>`,
   |                 which is not `Send`
help: `Send` can be made part of the associated future's guarantees for all
      implementations of `Store::save`
   |
 3 -     async fn save(&self, key: &str);
 3 +     fn save(&self, key: &str) -> impl std::future::Future<Output = ()> + Send;

编译器给出的修法就是标准答案:impl Future + Send 写返回类型,把 Send 要求写进 trait 契约。若你希望「同一份 trait 定义生成 Send 版本和 !Send 版本」, nightly 的 trait_variant 宏正是干这个的:

rust
// ⚠️ nightly-only 示例:trait_variant 目前需要 nightly 工具链,正文主线不使用
#[trait_variant::make(SendStore: Send)]
trait Store {
    async fn save(&self, key: &str);
}

🚀 进阶trait_variant 会把 Store 复制成「要求 SendSendStore」, 让库作者既能支持单线程轻量运行时,也能支持多线程 Tokio。


设计取舍:async、线程还是多进程

选型表

场景首选理由
大量并发网络 IO(网关、Web 服务、爬虫)async + Tokio单线程可管理上万连接,内存按连接数十字节计
少量并发(< 几十)、任务较重多线程 std::thread没有 async 传染性,调试简单(stack trace 完整)
CPU 密集(数值计算、编解码、压缩)多线程 + rayonasync 不会让 CPU 变快;要的是并行,不是挂起
CPU 密集且需要 IO(批处理服务)async 外壳 + spawn_blocking/rayon 内核各用各的长处
一次性脚本、CLI(几分钟内跑完)同步代码async 的错误处理与生命周期成本不划算
极端隔离/容错/多核扩展多进程进程崩溃不拖垮全局;配合消息队列做水平扩展
嵌入式 / WASMembassy / 无 OS executorstd 与 OS 线程都不可用
需要超时、取消、背压的组合asyncselect! / timeout / channel 背压是现成的
调用者几乎全是同步代码同步 + 线程池否则整条调用链都要 async 化

并发量到底需要多少

一个粗略的判断:用「同时在飞的 IO 请求数」而不是「QPS」来选模型

  • 峰值并发 < 一二百:std::thread 完全够用,代码更简单。
  • 峰值并发上千:async 的收益开始明显(省内存、省切换)。
  • 峰值并发上万:基本只有 async(或 Go 那样的有栈协程)扛得住。

「async 传染性」的现实影响

async 会沿调用链向上传染,这一点和 JS/Python 一样:

text
main                              <- 同步入口,只在这里 block_on 一次
  |
  +-- handler()        async
       |
       +-- service()   async
            |
            +-- repository()   async
                 |
                 +-- driver()  async   <- 链上任意一层是同步的,就要在中间 block_on

因此混合同步/异步的边界必须提前设计:

  • 边界上游(同步世界调 async):只在 main、线程池任务的入口处 block_on 一次。
  • 边界下游(async 调同步):spawn_blocking,不要在 async 函数体里直接阻塞。
  • 库作者:如果不知道用户用什么运行时,别依赖具体运行时的 IO 类型; 暴露 impl Future 或接受 &mut impl AsyncReadtokio::io 的 trait 有 futures-io 兼容层)。

什么时候不要用 async

  • 代码里几乎没有 IO(纯计算):async 只增加复杂度和 Pin/Send 报错,没有任何收益。
  • 并发度很低(每秒钟几个请求):线程模型的简单性更值钱。
  • 团队不熟悉:Send 报错、取消安全、Pin 都会成为长期 bug 来源; 先用 rayon + 线程池解决,收益不够再引入 async。
  • 需要「随时可中断、事务式回滚」的复杂业务逻辑:async 的取消语义是「drop」, 没有回滚,业务上需要自己做补偿。

与其他语言的对照

语言/机制语法形态调度方式与 Rust 的关键差异
JS Promise / asyncawait fetch()运行时自带单线程 event loopRust future 是惰性的;Promise 创建即执行
TypeScript Promise<T>类型化 Promise同上Rust 用 impl Future<Output = T>Output 是关联类型而非泛型参数
Python asyncioawait coro自带 event loop;GIL 使 CPU 无法并行Rust 无 GIL;future 可跨线程 Send,真并行
Python async def + TaskGroup结构化并发(3.11+)event loop 调度Rust 的 JoinSet 类似,但取消语义是 drop
C# Task<T> / asyncawait taskTaskScheduler + 线程池(内建)Rust 无内建调度器;C# 的 Task 默认已在跑(hot task)
C# ValueTask<T>避免分配同上接近 Rust 的「零分配状态机」,但仍是运行时驱动
Go goroutinego f() + channel运行时 M:N 调度,有栈协程Rust 状态机无独立栈、更省内存;Go 的 goroutine 无法被外部取消
Go select多 channel 竞速运行时tokio::select! 是宏,在编译期展开成 poll 状态机
Java 虚拟线程(21+)Thread.ofVirtual()JVM 调度,有栈无需改代码即可获得高并发;Rust 要显式 async 化
Java CompletableFuturethenCompose 回调链ForkJoinPoolRust 用 .await 线性写法,无回调金字塔
Kotlin 协程suspend fun + launchDispatchers 决定Kotlin 有结构化并发 coroutineScope;Rust 靠 JoinSet/scope
Kotlin Flow冷流 + 操作符协程调度对应 futures::Stream,但 Rust 的 stream 不在 std
C++20 协程co_await不提供调度器与 Rust 最像:语言只给机制,运行时自己接
Erlang/Elixir 进程抢占式轻量进程BEAM 调度Rust 的 future 是协作式:不 await 就不让出

💡 对照:最值得记住的一句话是——Rust 的 async 是「零成本抽象 + 外部运行时 + 无内置调度」。 语言只给你状态机和 Waker 协议(和 C++20 同一路线), 调度策略、IO 多路复用、线程模型全部由 crate 决定; 而 Go/Java 虚拟线程选择「有栈协程 + 内建调度」,代价是内存开销和运行时不可替换。


速查表

写法典型用法要点
async fn f() -> T定义异步函数返回 impl Future<Output = T>,惰性
fut.await等待结果只能在 async 上下文里用;忘写则什么都不执行
.await?传播错误需要统一错误类型或 From 转换
#[tokio::main]异步 main展开为建运行时 + block_on(flavor = "current_thread") 可切单线程
#[tokio::test]异步测试默认单线程运行时
Box::pin(fut)固定 future 地址递归 async、结构体里存 future 时必需
tokio::spawn(async move { .. })起并发任务要求 'static + Send;返回 JoinHandle
tokio::join!(a, b)并发等全部同一任务内并发,可借用局部变量
tokio::try_join!(a, b)并发等全部,失败即返回首个 Err 短路,其余 future 被 drop
tokio::select! { .. }竞速取第一个完成其余分支被 drop;biased; 改为按书写顺序
tokio::time::timeout(d, fut)加超时返回 Result<T, Elapsed>;超时即 drop fut
tokio::time::sleep(d)异步睡眠让出线程;别用 std::thread::sleep
tokio::task::spawn_blocking(f)跑阻塞代码专用阻塞线程池;std::fs/FFI/重 CPU 都放这里
tokio::sync::mpsc::channel(n)异步队列n 是 buffer 容量,满则 send().await 形成背压
tokio::sync::oneshot::channel()一次性应答常用于「任务完成后回传结果」
tokio::sync::Mutex::new(v)可跨 .await 的锁无竞争时比 std 版慢;能缩小临界区就优先用 std
Arc<Semaphore>::acquire_owned()限制并发数permit drop 即释放;Arc 让 permit 能进 'static 任务
futures::stream::iter(..).buffered(n)限流并发流buffered 保序,buffer_unordered 保吞吐
while let Some(x) = stream.next().await消费流需要 use futures::StreamExttokio_stream::StreamExt

内容以 rustc 1.98.1 · Rust 2024 edition 为基准