std::unique_lock<std::mutex> 硬核理解
通过"数到100"理解 std::mutex 与 std::unique_lock
多线程编程中,同步是绕不开的课题。当多个线程同时读写共享数据时,如果没有保护机制,结果往往是混乱的。
下面用一个最简单的例子——两个线程轮流数数——来直观感受 std::mutex 和 std::unique_lock 的作用。
问题:没有锁会发生什么?
假设两个线程同时对全局变量 num 做 ++num,没有同步措施:
// 危险代码:无锁保护
void increment() {
for (int i = 0; i < 50; ++i) {
++num; // 非原子操作!
std::cout << num << "\n";
}
}
++num 实际分三步:
- 读取
num的值 - 值加 1
- 写回内存
两个线程可能同时读到相同的值,各自加 1 后写回,结果就是漏数或重数。
| 时间线 | 线程A | 线程B | 实际 num |
|---|---|---|---|
| T1 | 读 num=5 |
5 | |
| T2 | 读 num=5 |
5 | |
| T3 | 写回 6 |
6 | |
| T4 | 写回 6 |
6(应该是7) |
50 + 50 次递增,最终结果可能只有 70、80,甚至更低。
解决:加锁保护
#include <mutex>
#include <thread>
#include <iostream>
class Counter {
public:
void increment(int times, int threadNo) {
for (int i = 0; i < times; ++i) {
// 构造时加锁,析构时自动解锁
std::unique_lock<std::mutex> lock(mtx);
++num; // 临界区:此时只有一个线程能访问 num
std::cout << "Thread " << threadNo
<< ": " << num << "\n";
} // lock 在这里销毁,自动释放 mtx
}
void run() {
std::thread t1(&Counter::increment, this, 50, 1);
std::thread t2(&Counter::increment, this, 50, 2);
t1.join();
t2.join();
std::cout << "Final: " << num << "\n"; // 一定是 100
}
private:
std::mutex mtx; // 互斥锁本体
int num = 0;
};
std::mutex vs std::unique_lock 分工
| 组件 | 角色 | 职责 |
|---|---|---|
std::mutex |
资源 | 被锁保护的对象,本身只提供 lock()/unlock() |
std::unique_lock |
管理者 | 封装 mutex,实现RAII(构造加锁、析构解锁) |
为什么用 unique_lock 而不是直接操作 mutex?
// 方式1:原始操作(容易出错)
mtx.lock();
++num;
// 如果这里抛出异常或提前 return,锁永远解不开了!
mtx.unlock();
// 方式2:RAII 管理(推荐)
{
std::unique_lock<std::mutex> lock(mtx); // 加锁
++num;
} // 无论正常结束还是异常,这里必定解锁
运行效果

的确不会乱,不会多数,也不会少数,但是细心的朋友会发现,这个要么是1号线程先数,要么是2号线程先数,那要轮换数数如何做呢?
可以实现交替数数(线程1 → 线程2 → 线程1 → 线程2...),但需要两个条件变量来互相唤醒。
核心思路
线程1: 数奇数 (1,3,5...) 线程2: 数偶数 (2,4,6...)
↓ ↓
数完唤醒2 数完唤醒1
自己等待 自己等待
交替数数代码
#include <mutex>
#include <thread>
#include <iostream>
#include <condition_variable>
class AlternatingCounter {
public:
void increment(int threadNo, int target) {
for (int i = 0; i < 50; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 等待轮到自己:thread1 数奇数,thread2 数偶数
cv.wait(lock, [this, target] {
return num % 2 == target; // target: 1=奇数轮, 0=偶数轮
});
++num;
std::cout << "Thread" << threadNo << ": " << num << std::endl;
// 数完唤醒另一个线程
cv.notify_one();
}
}
void run() {
std::thread t1(&AlternatingCounter::increment, this, 1, 1); // 目标奇数
std::thread t2(&AlternatingCounter::increment, this, 2, 0); // 目标偶数
t1.join();
t2.join();
std::cout << "Final: " << num << std::endl;
}
private:
std::mutex mtx;
std::condition_variable cv;
int num = 0;
};
预期输出效果
Thread1: 1 (奇数)
Thread2: 2 (偶数)
Thread1: 3 (奇数)
Thread2: 4 (偶数)
...
Thread1: 99
Thread2: 100
Final: 100
关键机制
| 组件 | 作用 |
|---|---|
num % 2 == target |
判断轮次:thread1 等奇数,thread2 等偶数 |
cv.wait() |
不满足条件就阻塞,释放锁让别人干活 |
cv.notify_one() |
数完立即唤醒对方,自己下一轮再等待 |
对比
| 方式 | 特点 | 适用场景 |
|---|---|---|
| 单锁竞争(你原来的) | 谁抢到谁数,顺序随机 | 高并发吞吐,不 care 顺序 |
| 双条件变量交替 | 严格轮询,顺序确定 | 需要协作、流水线、交替执行 |
注意:交替模式性能更低(频繁上下文切换),只适合需要严格顺序控制的场景。
运行效果

这以下这段代码解析一下:
cv.wait(lock, [this, target] {
return num % 2 == target;
});
执行到这个地方,如果不符合条件,return false,那么这个线程就挂起了(如果不挂起就空转,导致cpu资源浪费),
然后其实这个时候的lock是解锁状态,当其他的地方发送了notify,然后这个地方才被唤醒,唤醒以后加个锁,锁上开始往下执行,
如果没有以下代码:
[this, target] {
return num % 2 == target;
}
那就得这样写了:
while (num % 2 == threadNo)
{
cv.wait(lock);
}
因为这个地方又涉及到另外一个问题,“虚假唤醒”,感觉没完没了:)
虚假唤醒
什么是虚假唤醒(spurious wakeup):线程可能在没有被 notify 的情况下,从 wait() 中醒来。
为什么会这样? 是操作系统和硬件层面的特性,不是 C++ 特有的问题
| 原因 | 说明 |
|---|---|
| 操作系统实现 | POSIX 标准允许 pthread_cond_wait 不保证只被信号唤醒 |
| 信号中断 | Unix 信号(如 SIGINT)可能导致线程提前返回 |
| 多核 CPU 优化 | 某些架构为了性能,选择"宁可错杀"的策略 |
| 库/内核简化 | 实现方觉得严格保证"只被 notify 唤醒"成本太高 |
为什么标准允许虚假唤醒?
| 权衡 | 说明 |
|---|---|
| 性能 | 严格保证"只被 notify 唤醒"需要更多同步开销 |
| 复杂度 | 内核实现更简单、更高效 |
| 可移植性 | 不同平台行为统一(都可能有虚假唤醒) |
后记
通过这个篇幅,是不是对多线程处理有深刻的认识,如果对您有帮助,帮忙关注走一波!

浙公网安备 33010602011771号