一句话总结
异步里有两套锁:std::sync::Mutex(同步锁)和 tokio::sync::Mutex(异步锁).
默认仍用 std 同步锁--它更快;只有当必须跨 .await 持有锁时,才换 Tokio 异步锁.选错的代价是性能下降甚至死锁.
从上一篇的遗留问题接上
第 07 篇我们看到:跨 .await 持有 std::sync::MutexGuard 会因非 Send 而编译失败.当时给的首选解法是"在 await 前释放锁".但有时业务上确实绕不开--需要在持锁期间执行异步操作.这一篇就解决"那到底该用哪种锁".
两种 Mutex 的本质区别
两者都保护共享数据,区别在抢不到锁时怎么办:
std::sync::Mutex |
tokio::sync::Mutex |
|
|---|---|---|
| 抢不到锁时 | 阻塞当前线程 | 让出当前任务(其他任务继续) |
| 加锁方法 | lock()(同步) |
lock().await(异步) |
| 守卫能否跨 await | 不能(非 Send) | 能(Send) |
| 开销 | 低 | 较高(涉及任务调度) |
同步锁抢不到时,线程"原地干等"--但在异步世界,一个线程要养着成百上千个任务,让它干等等于全员陪绑.
异步锁抢不到时,只让"当前这个任务"靠边等,线程立刻去服务别人.
这正是同步与异步在第 02 篇就确立的那对本质差异,在锁上的再现.
反直觉的默认:优先用 std 同步锁
很多人以为"写异步代码就该全用异步锁",这是个误区.Tokio 官方文档明确建议:多数情况下应当用 std::sync::Mutex.理由是:
- 临界区(持锁期间的代码)通常极短--改个计数,推个元素,纳秒级完成.
- 这么短的持锁时间,锁竞争概率低,同步锁阻塞线程的代价可以忽略.
- 同步锁没有任务调度开销,比异步锁快得多.
|
判断准则
只在临界区内做纯内存操作,不跨 .await → 用 std::sync::Mutex.
这覆盖了实践中的绝大多数共享状态场景.
何时才该用 tokio::sync::Mutex
异步锁的唯一正当理由:必须在持锁期间执行 .await.例如,持有锁去访问一个受保护的异步资源:
|
这里持锁跨越了 .await,用 std 锁会编译失败;tokio::sync::Mutex 的守卫是 Send 的,正是为此设计.
即便用异步锁,持锁过 await 也要克制
异步锁让我们"能"跨 await 持锁,但不代表"该"长时间持有.持锁期间所有想加锁的任务都在排队,临界区里的 .await 越久(尤其别在里面做慢 IO),并发度塌得越狠.
能缩短就缩短,能不跨就不跨.
RwLock 与其他原语
同样的"同步/异步两套"格局也适用于读写锁,以及一些异步特有的协调原语:
| 原语 | 用途 | 选择要点 |
|---|---|---|
RwLock |
读多写少的共享数据 | 同 Mutex:不跨 await 用 std,跨则用 tokio |
tokio::sync::Semaphore |
限制并发数(如最多 10 个并发请求) | 异步专用,无同步对应 |
tokio::sync::Notify |
任务间的"等待/通知"信号 | 异步专用 |
Semaphore:一个会反复用到的工具
限流场景极常见--比如"对下游服务最多保持 10 个并发请求".Semaphore 发放固定数量的许可(permit),任务获取许可才能继续,用完归还.
这类"控制并发度"的需求,Go 里常用带缓冲 channel 模拟,Tokio 直接提供了语义更清晰的 Semaphore.
选择决策图
|
快速回顾
- 两套锁的差异:抢不到锁时,std 锁阻塞线程,tokio 锁让出任务;tokio 守卫 Send,可跨 await,但更慢.
- 默认用 std:临界区只做短暂内存操作,不跨 await 时,
std::sync::Mutex是更快的正确选择. - 用 tokio 锁的唯一理由:必须在持锁期间
.await. - 持锁过 await 要克制:即便用异步锁,也尽量缩短临界区,避免并发塌方.
- 其他原语:
RwLock同样分两套;Semaphore限并发数,Notify收发信号,均为异步专用.
动手练习
- std Mutex 共享计数:用
Arc<std::sync::Mutex<i32>>让 100 个 spawn 任务各自 +1,最后打印总和,确认为 100. - 复现报错:把上题的锁竞争部分故意写成跨
.await持有,观察编译错误,并说明编译器在保护什么. - 异步锁场景:构造一个"持锁期间必须 await"的场景,改用
tokio::sync::Mutex让它编译通过. - Semaphore 限流:用
tokio::sync::Semaphore限制"同时最多 3 个任务进入某段代码",验证效果. - 一句话总结:总结选锁的决策流程.