用同一个共享变量 counter,分别用 lock_guard 和 unique_lock 来保护它,并在代码里展示 unique_lock 的“更灵活”能力(延迟加锁、手动 unlock/relock、try_lock)。然后你编译运行就能直观看到区别。接着我会逐点解释两者差异。
用代码解释 unique_lock 的用法,以及和 lock_guard 的不同
我已经把你这个 test.cpp 改成可运行的对比示例了(3 个线程分别演示 lock_guard、unique_lock(defer_lock)、unique_lock(try_to_lock)),你运行看到:
- counter = 29990(不是固定值)
这是因为第三个线程用的是 try_to_lock:拿不到锁就不加,所以它可能少加一些,结果就会小于 3 * n。
1) lock_guard:最简单、最“死板”的 RAII 锁
代码(在 increment_with_lock_guard):
lock_guard<mutex> lock(mtx);
++counter;
它的特点:
- 构造时立即加锁
- 作用域结束自动解锁
- 不能手动 unlock()
- 没有 try_lock、没有延迟锁(就适合“锁住-干活-释放”这种最常见的临界区)
适用场景:
- 临界区非常明确且很短
- 不需要提前解锁/再加锁
- 不需要尝试加锁
2) unique_lock:更灵活(代价是稍微重一点)
2.1 defer_lock:延迟加锁(先创建锁对象,但先不锁)
你代码里(increment_with_unique_lock_defer):
unique_lock<mutex> lock(mtx, defer_lock); // 此刻不锁
// … 不需要锁的操作
lock.lock(); // 真正开始锁
++counter; // 临界区
lock.unlock(); // 可以提前解锁
它解决的问题是:
- 你可以把“耗时但不需要保护的操作”放在 lock.lock() 之前,减少持锁时间,提高并发性。
而 lock_guard 做不到延迟锁,它一创建就锁住了。
2.2 try_to_lock:尝试加锁(拿不到就不阻塞)
你代码里(try_increment_with_unique_lock):
unique_lock<mutex> lock(mtx, try_to_lock);
if (lock.owns_lock()) {
++counter;
}
这表示:
- 如果锁被别人占着,我不等,我就去做别的事(这里用 sleep 模拟)。
- 所以最终 counter 可能少加,输出不是固定值(你看到的 29990 就是这个原因)。
lock_guard 也做不到 try-lock(它没有这些构造策略)。
3) lock_guard vs unique_lock 核心区别总结
-
[是否能手动解锁]
- lock_guard: 不行(只能等作用域结束)
- unique_lock: 可以 unlock() / 再 lock()
-
[是否支持延迟加锁]
- lock_guard: 不支持
- unique_lock: 支持 defer_lock
-
[是否支持 try_lock]
- lock_guard: 不支持
- unique_lock: 支持 try_to_lock,并可用 owns_lock() 判断是否拿到锁
-
[开销/复杂度]
- lock_guard: 更轻、更简单(常用首选)
- unique_lock: 更重但更灵活(需要灵活控制时用)
当前文件状态
- 已完成:给你一个能跑的 demo,对比 lock_guard 和 unique_lock 的关键特性
- 下一步(如果你想验证):你可以把 try_increment_with_unique_lock 改成普通 lock.lock() 的版本,就会稳定输出接近 3 * n
为什么多次运行结果不一样?
因为你的程序里 try_increment_with_unique_lock() 使用了 try_to_lock(非阻塞尝试加锁),导致第三个线程 有时候拿不到锁就“跳过这次 ++counter”,所以总增量不是固定值。
关键代码
unique_lock<mutex> lock(mtx, try_to_lock); // 尝试加锁:拿不到就不阻塞
if (lock.owns_lock()) {
++counter; // 只有拿到锁才加 1
} else {
// 拿不到就做别的事(这里 sleep)
}
为什么“拿不到锁”的次数每次不同?
线程调度是不确定的(nondeterministic):
- 操作系统每次运行会以不同方式切换线程(时间片、优先级、CPU 核心分配等)。
- 你的另两个线程经常持有 mtx(哪怕很短),第三个线程恰好在那一瞬间来 try_to_lock:
- 可能拿到
- 也可能拿不到
- 拿不到就不加,因此每次“成功加 1 的次数”不同,结果就不同。
额外点:为什么不是 30000(3 * n)?
你设置 n = 10000,如果三个线程都必定成功加锁,理论上应该接近 30000。
但第三个线程用的是 “拿不到锁就跳过”,所以一般会变成:
counter = 20000 + (第三个线程成功次数)
所以你看到的 29977 / 29978 / 29989 都是合理的。
如果你想让结果每次都一样怎么办?
- 把 try_to_lock 改成阻塞加锁(保证每次都加到):
- 用 unique_lock<mutex> lock(mtx);(默认构造就会阻塞直到拿到锁)
- 或者直接用 lock_guard<mutex>
这样最终就会稳定输出 30000 左右(完全等于 30000,因为每次都做了 ++counter 且有锁保护)。
状态:已解释清楚“多次运行结果不同”的根因(try_to_lock + 线程调度不确定性 + 跳过增量)。
这里的lock_guard 在整个 for 循环中是一直存在的吗?还是每遍历一次都会加锁解锁一次呢?
不会一直存在:每次循环都会加锁/解锁一次
你这段代码:
void increment_with_lock_guard(int n) {
for (int i = 0; i < n; ++i) {
lock_guard<mutex> lock(mtx);
++counter;
}
}
发生了什么?
- lock_guard<mutex> lock(mtx); 是一个局部对象,它的作用域只在这一轮 for 循环的 { … } 这个块里。
- 每进入一轮循环:
- 构造 lock -> 自动 mtx.lock() 加锁
- 执行 ++counter
- 离开这一轮循环的 } -> lock 析构 -> 自动 mtx.unlock() 解锁
所以结论是:循环 n 次,就加锁/解锁 n 次。
如果想“整个 for 循环只加一次锁”怎么写?
把 lock_guard 放到循环外面,让它的作用域覆盖整个循环:
void increment_lock_once(int n) {
lock_guard<mutex> lock(mtx); // 只加锁一次
for (int i = 0; i < n; ++i) {
++counter;
} // 循环结束仍持锁
} // 函数结束才解锁
但要注意
- 这样做会让锁持有时间变长,其他线程更难抢到锁,并发性更差。
- 你原来的写法(每次只锁 ++counter)更常见,因为临界区很短。
unique_lock::lock() 和 unique_lock::unlock() 一定要成对吗?
unique_lock::lock() 和 unique_lock::unlock() 一定要成对吗?
1) “逻辑上”要成对:每次你手动 lock(),最终都必须有一次 unlock()
否则互斥量会一直被占用,其他线程会卡住(死锁/长期阻塞)。
但注意:对 unique_lock 来说,“成对”不一定是你手写一对,因为:
- unique_lock 析构函数会自动 unlock()(前提是它当前持有锁 owns_lock()==true)
- 所以你可以手动 lock(),然后不写 unlock(),让作用域结束自动解锁
示例:
{
unique_lock<mutex> lk(mtx);
// 临界区
} // 这里自动 unlock
2) 可以只用 unlock() 吗?
只能在你“已经持有锁”的前提下才能调用 unlock()。
也就是说:
- unique_lock<mutex> lk(mtx);(构造时就加锁)
这种情况下:可以直接 lk.unlock()(因为已经锁住了) - unique_lock<mutex> lk(mtx, defer_lock);(延迟锁定,没有锁)
这种情况下:不能直接 lk.unlock(),会出错(未持锁却解锁,属于逻辑错误,标准规定是未定义行为/可能抛异常/直接崩)
用你文件里的写法举例:
unique_lock<mutex> lock(mtx, defer_lock); // 此刻 owns_lock()==false
lock.unlock(); // ❌ 错:还没 lock() 就 unlock()
3) 如何安全判断能不能 unlock()?
用 owns_lock():
if (lock.owns_lock()) {
lock.unlock();
}
总结
- [需要成对吗]
- 手动 lock() 后必须“最终释放一次”,可以是手动 unlock(),也可以靠析构自动释放。
- [能只用 unlock 吗]
- 只有在 unique_lock 当前已经持锁(owns_lock()==true)时才可以;否则不行。






