将notify_one()置于锁定范围内能否避免线程竞争问题?
我实现了一个包装类LockableOperationT,用于在请求方线程和执行操作的提供方线程之间处理同步操作:请求方线程调用waitUntilProcessed()等待操作完成,提供方线程执行process()完成操作后通知请求方。
原代码实现
template <class tBridgeDeclType> class LockableOperationT : public OperationSignatureT<tBridgeDeclType> { public: explicit LockableOperationT(const OperationSignatureT<tBridgeDeclType> &fOperation) : mOperation(fOperation) { } virtual ~LockableOperationT() { } void waitUntilProcessed() { // Lock the bridge from the requiring side. std::unique_lock<std::mutex> processLock(mMutex); // Wait until operation has been processed and take care of spurious wake ups. mConditionVariable.wait(processLock, [this]{ return(mHasBeenProcessed); }); } void process(void) override final { // Process operation. Have to remove const to do this const_cast<OperationSignatureT<tBridgeDeclType> &>(mOperation).process(); // Lock the bridge from the providing side. { std::lock_guard<std::mutex> processLock(mMutex); mHasBeenProcessed = true; } // Requiring side is notified that the operation has been processed mConditionVariable.notify_one(); } // Members protected: const OperationSignatureT<tBridgeDeclType> &mOperation; bool mHasBeenProcessed = false; std::mutex mMutex; std::condition_variable mConditionVariable; };
执行流程
请求方发送操作给提供方后等待完成,提供方执行操作后通知请求方,请求方收到通知后销毁LockableOperationT对象,符合RAII原则。
原代码的竞态条件
- 请求方线程(Thread-1)调用
waitUntilProcessed(),加锁并等待mHasBeenProcessed为true; - 提供方线程(Thread-2)执行
process(),设置mHasBeenProcessed为true后退出锁定范围; - Thread-1发生虚假唤醒,检测到
mHasBeenProcessed为true后返回,请求方销毁LockableOperationT对象; - Thread-2继续执行,调用已销毁对象的
mConditionVariable.notify_one(),引发未定义行为。
疑问
将notify_one()移动到锁定范围内(如下代码所示),能否避免该竞态条件,还是仅会将竞态转移到其他位置?
// Lock the bridge from the providing side. { std::lock_guard<std::mutex> processLock(mMutex); mHasBeenProcessed = true; // Requiring side is notified that the operation has been processed mConditionVariable.notify_one(); }
解答
把notify_one()移到锁范围内确实能解决这个特定的竞态条件,不会转移到其他位置,核心原因如下:
锁的同步与内存可见性:当Thread-2在锁内完成
mHasBeenProcessed = true和notify_one()的调用时,互斥锁会保证这些操作的内存可见性——Thread-1在wait返回时看到的mHasBeenProcessed状态一定是最新的。同时,wait在返回前会重新获取锁,这意味着Thread-1必须等到Thread-2释放锁之后,才能退出wait并执行后续的对象销毁逻辑。彻底避免对象提前销毁:Thread-1只有在Thread-2完全释放锁(即已经完成锁内的所有操作,包括
notify_one())之后,才会离开waitUntilProcessed()并销毁对象。这样就从根本上杜绝了Thread-2调用已销毁对象的notify_one()的情况。
另外需要注意:原代码中使用const_cast去除mOperation的const属性存在风险,如果传入的mOperation实际是const对象,修改它会触发未定义行为。建议调整mOperation的类型为非const引用,或者在设计层面确保传入的操作允许被修改。
内容的提问来源于stack exchange,提问作者Nilsie

