Appearance
C++ 标准库提供的 Mutex 类型详解
系统梳理 C++11 以来
<mutex>/<shared_mutex>提供的全部互斥量类型,对比它们的语义、底层实现、性能特征与适用场景,并给出选型决策树。
阅读时间:2026-08-05
1. 本篇要回答的问题
- C++ 标准库到底提供了哪几种 mutex?它们各自解决什么问题?
std::mutex/std::recursive_mutex/std::timed_mutex/std::recursive_timed_mutex/std::shared_mutex/std::shared_timed_mutex之间到底有什么区别?- 普通锁、递归锁、定时锁、读写锁在底层实现和性能上有什么差异?
- 实际工程中应该怎么选型?
std::mutex不够用时该升级到哪一个?
2. Mutex 类型全景
C++ 标准库(C++11 起)在头文件 <mutex> 和 <shared_mutex> 中定义了以下互斥量类型:
| 类型 | 引入标准 | 头文件 | 是否可重入 | 是否可超时 | 是否支持读共享 |
|---|---|---|---|---|---|
std::mutex | C++11 | <mutex> | 否 | 否 | 否 |
std::recursive_mutex | C++11 | <mutex> | 是 | 否 | 否 |
std::timed_mutex | C++11 | <mutex> | 否 | 是 | 否 |
std::recursive_timed_mutex | C++11 | <mutex> | 是 | 是 | 否 |
std::shared_timed_mutex | C++14 | <shared_mutex> | 否 | 是 | 是(读写锁) |
std::shared_mutex | C++17 | <shared_mutex> | 否 | 否 | 是(读写锁) |
注:
std::shared_timed_mutex在 C++14 引入,std::shared_mutex在 C++17 引入(相当于"不可超时的shared_timed_mutex")。两者的读写锁语义完全一致,区别仅在是否支持try_lock_for/try_lock_until。
所有类型都遵循同一组互斥量接口约定(BasicLockable / Lockable / TimedLockable / SharedLockable),因此可以被 std::lock_guard / std::unique_lock / std::shared_lock 等 RAII 包装器统一接管。
3. 逐类详解
3.1 std::mutex —— 基础互斥量(最常用)
cpp
#include <mutex>
std::mutex m;
void worker() {
m.lock(); // 阻塞直到获得锁
// 临界区
m.unlock(); // 必须手动配对,忘了解锁 = 死锁风险
}实际工程中永远用 RAII 包装,不要手写 lock/unlock:
cpp
void worker() {
std::lock_guard<std::mutex> lk(m); // 构造即加锁,析构即解锁
// 临界区
}- 语义:同一时刻只允许一个线程持有锁;同一线程不可重复加锁(重复
lock()是未定义行为,在多数实现上会死锁或抛std::system_error)。 - 底层:Linux/glibc 上通常直接映射到 pthread_mutex(非递归、默认类型),在无竞争时走 futex 快速路径(用户态 CAS),竞争时才陷入内核。
- 性能:开销最低,是所有锁类型的基准。
- 适用:绝大多数"一段临界区、一个线程进入"的场景。
3.2 std::recursive_mutex —— 可重入互斥量
cpp
std::recursive_mutex rm;
void foo() {
std::lock_guard<std::recursive_mutex> lk(rm);
bar(); // bar() 内部也会对 rm 加锁
}
void bar() {
std::lock_guard<std::recursive_mutex> lk(rm); // 同一线程再次加锁 = OK
// 临界区
}- 语义:同一线程可以多次
lock(),必须配对相同次数的unlock()(通常靠 RAII 自动完成)才能释放;不同线程之间仍是互斥的。 - 底层:pthread_mutex 的
PTHREAD_MUTEX_RECURSIVE类型,内部维护"持有线程 ID + 递归计数"。 - 代价:比
std::mutex略重(每次加锁都要检查/更新 owner 与计数)。 - 适用:递归函数需要持锁、或公共接口与内部实现共用同一把锁且难以拆分时。但更优解是重构代码消除重入需求,递归锁往往掩盖了设计缺陷(持锁范围过大、接口粒度不清)。
- 关键限制:递归计数通常有上限(实现定义),超限会返回错误/抛异常;且绝对不能跨线程传递所有权。
3.3 std::timed_mutex —— 可超时互斥量
cpp
std::timed_mutex tm;
void worker() {
// 相对超时:最多等 100ms
if (tm.try_lock_for(std::chrono::milliseconds(100))) {
// 成功获得锁
tm.unlock();
}
// 绝对超时:等到某个时间点
if (tm.try_lock_until(std::chrono::steady_clock::now() + std::chrono::seconds(1))) {
tm.unlock();
}
// 非阻塞尝试(立即返回)
if (tm.try_lock()) {
tm.unlock();
}
}- 语义:在
std::mutex基础上新增try_lock_for/try_lock_until两个带超时的尝试加锁接口;超时未获得锁则返回false而不会永久阻塞。同时继承try_lock()(立即返回,非阻塞)。 - 底层:pthread_mutex +
pthread_mutex_timedlock(基于CLOCK_MONOTONIC),超时基于单调时钟,不受系统时间回拨影响。 - 适用:
- 不能无限等待(避免死锁导致整个线程卡死),超时后走降级/重试/报错逻辑;
- 需要"尝试拿锁,拿不到就先干点别的"的非阻塞协作;
- 诊断/测试场景(探测是否发生死锁)。
3.4 std::recursive_timed_mutex —— 递归 + 超时
cpp
std::recursive_timed_mutex rtm;
void foo() {
if (rtm.try_lock_for(std::chrono::milliseconds(50))) {
bar(); // 内部可再次加锁(递归)
rtm.unlock();
}
}- 语义:
std::recursive_mutex与std::timed_mutex的能力合集——既可重入,又可超时尝试。 - 适用:极少见的"既需要递归、又需要超时"的场景。如果用到它,建议先反思设计是否该拆锁或换方案。
3.5 std::shared_mutex(C++17)/ std::shared_timed_mutex(C++14)—— 读写锁
cpp
std::shared_mutex sm;
// 写者:独占
void write() {
std::unique_lock<std::shared_mutex> lk(sm); // lock()
// 修改共享数据
}
// 读者:共享
void read() {
std::shared_lock<std::shared_mutex> lk(sm); // lock_shared()
// 多个读者可同时进入
}- 语义:提供两种加锁模式:
- 独占模式(unique):
lock()/try_lock(),写者使用,与其他写者、所有读者互斥; - 共享模式(shared):
lock_shared()/try_lock_shared(),读者使用,多个读者可并发持有。
- 独占模式(unique):
- 读写锁的核心价值:"读多写少"场景下,把读者之间的互斥放开,只保留读者与写者的互斥,从而大幅提升并发读吞吐。
std::shared_mutex:C++17,只支持lock/lock_shared(无超时),实现通常基于读写偏向的 futex 或 OS 原生 rwlock(Linux 上 glibc 用PTHREAD_RWLOCK)。std::shared_timed_mutex:C++14,额外支持try_lock_for/try_lock_until/try_lock_shared_for/try_lock_shared_until。- 写者饥饿风险:标准不保证读/写公平性。多数实现偏向读者(读优先),高并发读时写者可能长时间拿不到锁(写饥饿)。若写延迟敏感,需要业务层限流或选用支持写优先的第三方实现(如
boost::shared_mutex某些策略、或folly::RWTicket64)。 - 适用:配置表、缓存、路由表等"读远多于写"的共享数据结构。
4. 底层实现与性能特征

实现要点:
- 快速路径(无竞争):所有类型都优先走用户态原子操作(CAS / 原子 exch),不进内核。
std::mutex在无竞争下加锁约为几次原子指令(~10~20ns 量级)。 - 慢速路径(有竞争):拿不到锁时基于 futex(
FUTEX_WAIT)陷入内核睡眠,让出 CPU,避免忙等;唤醒由FUTEX_WAKE触发。 - 递归锁额外开销:每次加锁要读 owner、比较线程 ID、维护递归计数,慢路径和快路径都比
std::mutex重。 - 读写锁:内部通常维护"读者计数 + 写者标志",读加锁/解读锁是原子计数增减(快),写加锁需等所有读者退出(慢)。
经验性开销排序(同硬件,无/低竞争):
bash
std::mutex < std::timed_mutex ≈ std::recursive_mutex
< std::shared_mutex(读) ≈ std::shared_timed_mutex(读)
< std::recursive_timed_mutex < shared_mutex(写) ≈ mutex(写)注意:这是"加锁原语本身"的开销排序,真实性能瓶颈几乎永远在临界区大小与锁竞争程度,而非锁类型选择。
5. C++ 的几种"锁包装器"(RAII Lock)—— 实际编码真正用的东西
前面 §2~§3 讲的是互斥量本体(mutex 对象)。但工程里你几乎从不直接写 m.lock() / m.unlock(),而是用RAII 包装器接管锁的生命周期——构造即加锁、析构即解锁,异常安全且不可能忘解锁。标准库提供 4 种:
| 包装器 | 引入标准 | 是否可延迟加锁 | 是否可手动 unlock | 适用锁类型 | 加锁模式 |
|---|---|---|---|---|---|
std::lock_guard | C++11 | 否 | 否 | 任意 BasicLockable(含全部 mutex) | 独占 |
std::unique_lock | C++11 | 是(defer_lock) | 是 | 任意 mutex(独占) | 独占 |
std::shared_lock | C++14 | 是 | 是 | shared_mutex 等 SharedLockable | 共享(读) |
std::scoped_lock | C++17 | 否 | 否 | 任意数量 mutex | 独占(多锁原子获取) |
5.1 std::lock_guard —— 最轻量、最常用
cpp
std::mutex m;
void f() {
std::lock_guard<std::mutex> lk(m); // 构造即 m.lock()
// 临界区
} // 析构即 m.unlock(),无论正常返回还是抛异常- 语义:构造时加锁、析构时解锁,不可延迟、不可手动解锁、不可移动。零额外开销,是"单纯保护一段临界区"的首选。
- 标签参数:
std::adopt_lock表示"锁已经被我拿着了,你只接管解锁";配合std::lock()防死锁(见 §5.5)。 - 适用:99% 的普通加锁场景。
5.2 std::unique_lock —— 灵活但略重
cpp
std::mutex m;
void f() {
std::unique_lock<std::mutex> lk(m, std::defer_lock); // 构造不加锁
// ... 做些不持锁的准备工作 ...
lk.lock(); // 这里才真正加锁
// 临界区
lk.unlock(); // 可提前解锁
// ... 不持锁的后处理 ...
} // 若仍持有,析构时解锁- 语义:比
lock_guard多三个能力——可延迟加锁(defer_lock)、可手动unlock()、可移动(move)。代价是内部多存一个"是否持有锁"的状态位,开销略大。 - 标签参数:
std::defer_lock(构造不加锁)、std::try_to_lock(构造时try_lock)、std::adopt_lock(接管已持有的锁)。 - 适用:需要延迟加锁、提前解锁、或把锁 move 给别的函数/容器(如条件变量
std::condition_variable::wait(lk)必须用unique_lock)。
5.3 std::shared_lock —— 读者侧的共享锁包装
cpp
std::shared_mutex sm;
void reader() {
std::shared_lock<std::shared_mutex> lk(sm); // 构造即 lock_shared()
// 多个读者可同时持有
}- 语义:专用于
shared_mutex/shared_timed_mutex的共享模式(读),接口与unique_lock类似(可延迟/手动解锁/移动)。 - 适用:读多写少场景中,读者用它;写者侧仍用
unique_lock(独占lock())。 - 注意:不能在
shared_lock持有期间"升级"为写锁,标准无升级锁,强行lock()会自死锁。
5.4 std::scoped_lock —— C++17 多锁防死锁的一站式方案
cpp
std::mutex m1, m2;
void f() {
std::scoped_lock lk(m1, m2); // C++17 类模板实参推导,原子地同时锁住两把
// 临界区(同时持有 m1、m2,不会交叉死锁)
}
// 析构按逆序解锁- 语义:
scoped_lock是对std::lock(m1, m2, ...)+ 多把lock_guard(adopt_lock)的语法糖——一次性以确定性顺序获取任意数量的锁,彻底避免交叉加锁死锁。 - 优势:比手写
std::lock()+lock_guard(..., adopt_lock)简洁;支持可变数量锁(variadic);C++17 起可省略模板参数(CTAD 自动推导)。 - 适用:需要同时持多把锁的函数,优先用它而非手动
std::lock。
5.5 死锁防护:多锁如何安全获取
交叉加锁死锁(线程A持m1等m2,线程B持m2等m1)的经典解法:
cpp
std::mutex m1, m2;
// 方案 A(C++11,显式 std::lock + adopt_lock)
void legacy() {
std::lock(m1, m2); // 原子地同时获取,内部用死锁避免算法
std::lock_guard<std::mutex> g1(m1, std::adopt_lock);
std::lock_guard<std::mutex> g2(m2, std::adopt_lock);
}
// 方案 B(C++17,scoped_lock 一句话搞定,推荐)
void modern() {
std::scoped_lock lk(m1, m2);
}| 写法 | 标准 | 评价 |
|---|---|---|
手写 std::lock + lock_guard(adopt_lock) | C++11 | 正确但啰嗦 |
std::scoped_lock | C++17 | 推荐,一行搞定且可锁任意多把 |
5.6 包装器与互斥量本体对应总表
| 互斥量本体 | 写者侧包装 | 读者侧包装 |
|---|---|---|
std::mutex / timed_mutex / recursive_* | lock_guard / unique_lock | — |
std::shared_mutex / shared_timed_mutex | unique_lock(独占 lock()) | shared_lock(共享 lock_shared()) |
| 多把任意 mutex 同时获取 | scoped_lock | — |
经验法则:默认
lock_guard;要延迟/提前解锁或用条件变量用unique_lock;读shared_mutex用shared_lock;同时锁多把用scoped_lock。它们都不改变底层 mutex 的语义,只是帮你安全地管理锁的生命周期。
6. 选型决策树
bash
┌─────────────────────────────┐
│ 需要多个读者并发? │
└──────┬──────────────────────┘
│
┌────────────┴────────────┐
│ 是 │ 否
▼ ▼
┌──────────────────┐ ┌─────────────────────────┐
│ shared_mutex │ │ 同一线程会重入同一把锁? │
│ (需超时用 │ └──────┬──────────────────┘
│ shared_timed) │ │
└──────────────────┘ ┌──────┴────────┐
│ 是 │ 否
▼ ▼
┌──────────────┐ ┌──────────────────────┐
│ recursive_ │ │ 需要 try_lock_for / │
│ mutex │ │ try_lock_until 超时?│
│ (需超时用 │ └──────┬───────────────┘
│ recursive_ │ │
│ timed) │ ┌──────┴──────┐
└──────────────┘ │ 是 │ 否
▼ ▼
┌──────────────┐ ┌──────────┐
│ timed_mutex │ │ std::mutex│
└──────────────┘ └──────────┘一句话记忆:默认 std::mutex;读多写少用 shared_mutex;必须重入才上 recursive_mutex;要超时退避用 timed_mutex;三者罕见组合才考虑 recursive_timed_mutex 与 shared_timed_mutex。
7. 常见陷阱
std::mutex重复加锁 = 未定义行为:同一线程二次lock()在 glibc 上通常死锁(pthread 默认非递归)。需要重入就显式用recursive_mutex,不要依赖"碰巧没死锁"。- 裸
lock/unlock忘记配对:异常路径下极易漏unlock→ 用lock_guard/unique_lock永远是对的。 - 递归锁掩盖设计坏味道:能用"缩小临界区 + 拆分接口"消除重入,就不要长期依赖递归锁。
- 读写锁的写饥饿:高读并发下写者可能饿死;对写延迟敏感的场景要测公平性,必要时换写优先实现或缩短读临界区。
shared_mutex写者不保护读者重入:读者持shared_lock时不能升级为写,否则自死锁;标准不提供"升级锁"。- 跨线程转移所有权:
recursive_mutex的递归计数绑定线程,绝不能把一个线程里"加了一半"的递归锁丢给另一线程解锁。 - 误把
try_lock()当lock():try_lock失败立即返回false,不阻塞——忘记判返回值会带着"没拿到锁"进入临界区,造成数据竞争。
8. 与无锁/RCU 的关系
mutex 是"阻塞式互斥"的代表,属于悲观并发控制。当锁竞争成为延迟瓶颈时,演进方向是:
- 减小临界区 / 缩短持锁时间:最常做、收益最大;
- 读写锁放开读者并发;
- 无锁(Lock-Free):用 CAS/原子操作消除阻塞(见 lockfree-deep.md);
- RCU:读者完全无等待,写者走"发布-订阅 + 宽限期"回收(见 rcu-internals.md)。
换句话说,本文的 mutex 家族是"先保证正确",而 lockfree/RCU 是"在保证正确之后追求极致延迟"的下一步。
9. 一句话总结
C++ 标准库提供了 6 种互斥量,本质是在**"是否可重入 / 是否可超时 / 是否支持读写共享"**三个维度上的能力组合;默认用 std::mutex,读多写少升级 shared_mutex,需要重入或超时再分别上 recursive_* / timed_*,而锁之外的更优解往往是"缩小临界区"或走向无锁/RCU。
关联文档
- lockfree-deep.md —— 无锁编程深入:CAS/Wait-Free/Lock-Free 分级
- rcu-internals.md —— RCU 原理与实现,读者无等待的另一种思路
- low-latency-patterns.md —— 低延时设计模式全景(含忙等 vs 睡眠调度)
- memory-model.md —— 原子操作与内存模型,理解 mutex 之下的 acquire/release 语义