bigywater.tech
Rust · 103

异步 Mutex 的使用边界

异步代码里该用 std::sync::Mutex 还是 tokio::sync::Mutex?答案不是「异步就用异步的锁」。这篇用编译错误、真实死锁和 400 万次加锁的计时,说清楚两者各自的边界。

used across an await
编译器指出 std MutexGuard 导致 Future 不是 Send 的原因
死锁
单线程运行时里 std Mutex 跨 await 持有的结局
5.2×
无 await 的短临界区里,tokio Mutex 比 std Mutex 慢的倍数

std MutexGuard 跨 await

实测 · 本机执行输出
  error: future cannot be sent between threads safely
     --> src/main.rs:5:5
      |
    5 |     tokio::spawn(async move { let mut g = m.lock().unwrap(); *g += 1; tokio::time::sleep(std::time::Duration::from_millis(1)).await; }).aw...
      |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ future created by async block is not `Send`
      |
      = help: within `{async block@src/main.rs:5:18: 5:28}`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, i32>`
  note: future is not `Send` as this value is used across an await
     --> src/main.rs:5:127
      |
    5 | ...c move { let mut g = m.lock().unwrap(); *g += 1; tokio::time::sleep(std::time::Duration::from_millis(1)).await; }).await.unwrap();
      |                 ----- has type `std::sync::MutexGuard<'_, i32>` which is not `Send`                         ^^^^^ await occurs here, with `mut g` maybe used later
  note: required by a bound in `tokio::spawn`
     --> <实验目录>/cargo_home/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/task/spawn.rs:176:21

Send 的规则见Send 与 Sync;spawn 的约束见spawn 与 'static。

tokio::sync::Mutex

实测 · 本机执行输出
  编译退出码 0
  4 个任务各自 持锁 → sleep 20 ms(跨 await)→ +1,最终值 4(没有丢更新)

不跨 await 的 std Mutex

实测 · 本机执行输出
  编译退出码 0
  std Mutex 只在块内持有,任务可以 spawn:[3, 2, 1, 0]

单线程上的死锁

实测 · 本机执行输出
  编译退出码 0 []
  6 秒内程序没有退出:B 阻塞了唯一的线程,连 500 ms 的 timeout 也没有机会触发 → 死锁

阻塞线程的更多后果见阻塞代码混入异步任务。

性能

实测 · 本机执行输出
  编译退出码 0
  std::sync::Mutex   4 任务 × 100 万次:    75.5 ms
  tokio::sync::Mutex 4 任务 × 100 万次:   389.7 ms

这是 macOS aarch64 上的一次微基准,只测了高争用的短临界区。

分步演示

异步 Mutex
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。tokio 1.x
    • 没有验证:跨 await 持有 tokio Mutex 的公平性、parking_lot Mutex 的行为、try_lock 的用法、RwLock 的异步版本、LocalSet 里 !Send Future 的其它风险、死锁时具体卡在哪一行(没有用调试器附加)
    京ICP备2026067502号-1京公网安备11010502063487号