MutexGuard 跨越 await 的问题?

解读

在国内 Rust 后端/中间件面试中,“MutexGuard 能不能安全地跨 await” 几乎是并发与异步章节的必考题。
面试官真正想听的是:

  1. 你是否理解 Send + Sync 对异步任务调度的影响;
  2. 你是否知道 MutexGuard 的 Drop 顺序await 点 可能产生死锁或 UB;
  3. 你是否能给出 零成本、无运行时开销 的改写方案。
    一句话:编译器报错只是表象,背后是对 Send 语义与调度器迁移的深层考察

知识点

  1. MutexGuard 不是 Send:标准库中 impl<T: ?Sized> !Send for MutexGuard<'_, T>,因为 guard 里存了指向内核对象的内指针,跨线程后地址失效。
  2. 异步任务可能被调度到不同线程:每次 await 后,Future 可能被另一个线程 poll,若 guard 仍在栈上,新线程拿不到锁却拿到指针,触发 data race。
  3. 编译器报错信息future created by async block is not Send;帮助信息里会列出 MutexGuard<?> 不满足 Send。
  4. 死锁风险:即使通过 unsafe impl Send 强行过关,guard 在旧线程释放,锁的唤醒队列可能通知错线程,造成永久阻塞
  5. 正确范式“锁只保护临界区,不保护异步等待”;用 std::sync::Mutex 时,临界区里禁止任何 await;需要异步粒度锁,改用 tokio::sync::MutexRAII + drop 前置 技巧。

答案

场景代码(错误示范)

use std::sync::Mutex;
use tokio::time::{sleep, Duration};

async fn bad() {
    let data = Mutex::new(vec![1, 2, 3]);
    let guard = data.lock().unwrap();
    sleep(Duration::from_millis(10)).await; // 编译错误:future 不是 Send
    println!("{:?}", *guard);
}

根本原因
guard 的类型是 MutexGuard<'_, Vec<i32>>,它不是 Send,而 .await 可能导致任务跨线程迁移,编译器拒绝生成 Send 的 Future

修正方案一:缩小临界区(零成本,推荐)

async fn good1() {
    let data = std::sync::Mutex::new(vec![1, 2, 3]);
    {
        let mut v = data.lock().unwrap();
        v.push(4);
        // 临界区结束立即 drop
    }
    sleep(Duration::from_millis(10)).await;
    let v = data.lock().unwrap();
    println!("{:?}", *v);
}

修正方案二:使用异步锁(tokio 生态)

use tokio::sync::Mutex; // 注意:这是 tokio::sync::Mutex
async fn good2() {
    let data = Mutex::new(vec![1, 2, 3]);
    let mut v = data.lock().await;
    v.push(4);
    sleep(Duration::from_millis(10)).await; // 允许跨 await
    println!("{:?}", *v);
}

结论
std::sync::Mutex 的 guard 绝不允许跨越 await;要么提前 drop,要么换 tokio::sync::Mutex;后者内部使用队列 + Waker 机制,锁本身支持异步等待,因此 guard 是 Send 的。

拓展思考

  1. 性能对比:tokio::sync::Mutex 每次 lock 需要一次原子队列入队,比 std 版本慢 2~3 倍;在高并发、临界区只涉及内存操作的场景,优先用 std 锁 + 缩小作用域。
  2. 读写分离:如果数据读多写少,可改用 tokio::sync::RwLockparking_lot::RwLock读锁 guard 是 Send(parking_lot 版本),可跨 await 读快照。
  3. !Send 类型通用法则:除 MutexGuard 外,RefCellRef/RefMutRc 等都不是 Send;在 async 块里一旦持有它们,整个 Future 都会变成 !Send;写库时若需强制 Send,可用 static_assertions::assert_impl_send! 做 CI 检查。
  4. 嵌入式场景:no_std + alloc 时无 tokio,可用 临界区 + 中断屏蔽spin::Mutex;此时禁止任何 await,因为根本没有调度器,问题退化为“中断与主循环共享数据”的经典场景。