当同一互斥锁在其他地方与lock_guard配合时,用unique_lock做wait/notify是否安全?
首先可以明确告诉你:只要所有操作mutexBar的代码都正确使用锁包装器(不管是std::lock_guard还是std::unique_lock),你的wait/notify操作就是完全安全的。下面我来拆解背后的逻辑和需要注意的细节:
核心原理:锁包装器只是互斥量的"工具人"
std::mutex本身是一个底层的同步原语,std::lock_guard和std::unique_lock只是封装它的工具类,本质都是操作同一个mutexBar:
lock_guard是轻量级的,构造时自动锁定,析构时自动解锁,适合简单的临界区场景;unique_lock更灵活,支持手动解锁、转移所有权,这也是condition_variable::wait必须用它的原因——wait过程中会自动释放锁并挂起线程,被唤醒时又会重新获取锁,lock_guard做不到这点。
不管用哪种包装器,只要是正确锁定了mutexBar,互斥量都会保证同一时间只有一个线程持有它,这是线程安全的基础。
你的现有代码的安全性分析
先看你提供的几个方法:
Foo::wait():
这里的std::unique_lock<std::mutex> waitPoint(mutexBar);会自动锁定mutexBar,所以if (waitPoint.owns_lock())其实是冗余的(除非你用了std::defer_lock参数,否则owns_lock()必然为true)。不过这不影响安全性,condBiz.wait(waitPoint)的调用是完全符合规范的——必须在持有锁的情况下调用wait,它会正确释放锁并等待,唤醒后重新锁定。Foo::signal():
锁定mutexBar后调用notify_all()是安全的。虽然C++标准允许不持有锁调用notify,但持有锁调用也没问题,只是可能导致被唤醒的线程立刻阻塞到锁释放(这是性能层面的小问题,不是安全问题)。Foo::safeSection():
用unique_lock保护临界区,和用lock_guard的效果完全一致,都是正确的同步方式。
其他场景用lock_guard的影响
如果其他代码里用std::lock_guard<std::mutex> lg(mutexBar);来操作这个互斥量,完全不会破坏wait/notify的安全性:
- 当其他线程持有
lock_guard锁定的mutexBar时,你的wait()或signal()里的unique_lock会自动阻塞,直到lock_guard析构解锁,这是正常的同步逻辑,没有任何安全风险; - 反之,如果你的
wait()或signal()持有锁,其他线程的lock_guard也会阻塞等待,同样是安全的。
一个必须提醒的坑:避免用if判断等待条件
你的wait()方法里用if检查锁的持有状态没问题,但如果之后要添加业务层面的等待条件(比如等待某个变量达到特定值),绝对不能用if判断,必须用while循环。比如正确的写法应该是:
void Foo::wait() { std::unique_lock<std::mutex> waitPoint(mutexBar); // 用while循环防止虚假唤醒 while (!this->isConditionMet()) { // 替换成你的实际条件判断 condBiz.wait(waitPoint); } }
这是因为条件变量可能会出现虚假唤醒(线程被系统唤醒,但并不是因为notify调用),或者被唤醒后等待的条件已经被其他线程修改了,所以必须重新检查条件。
内容的提问来源于stack exchange,提问作者huseyin tugrul buyukisik

