欢迎光临
我们一直在努力

用代码解释 unique_lock 的用法,以及和 lock_guard 的不同

用同一个共享变量 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)时才可以;否则不行。
赞(0)
未经允许不得转载:171主机测评 » 用代码解释 unique_lock 的用法,以及和 lock_guard 的不同
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址