标准::condition_variable 中可能存在的争用条件
Possible race condition in std::condition_variable?
我研究了std::condition_variable(lock,pred)
的VC++实现,基本上是这样的:
template<class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)
{ // wait for signal and test predicate
while (!_Pred())
wait(_Lck);
}
基本上,裸wait
调用_Cnd_waitX
调用_Cnd_wait
调用do_wait
调用cond->_get_cv()->wait(cs);
(所有这些都在文件中 cond.c)。
cond->_get_cv()
返回Concurrency::details::stl_condition_variable_interface
.
如果我们去 文件primitives.h
,我们看到在Windows 7及更高版本下,我们有类stl_condition_variable_win7
,其中包含旧的良好win32CONDITION_VARIABLE
,并且wait
调用__crtSleepConditionVariableSRW
。
做一些程序集调试,__crtSleepConditionVariableSRW
提取SleepConditionVariableSRW
函数指针,然后调用它。
事情是这样的:据我所知,win32CONDITION_VARIABLE
不是内核对象,而是用户模式对象。因此,如果某个线程通知此变量,并且实际上没有线程在其上休眠,则您丢失了通知,并且线程将保持休眠状态,直到超时或其他线程通知它。一个小程序实际上可以证明这一点 - 如果你错过了通知点 - 你的线程将保持睡眠状态,尽管其他线程通知了它。
我的问题是这样的:
一个线程等待条件变量,谓词返回 false。然后,发生上面解释的整个调用链。在此期间,另一个线程更改了环境,因此谓词将返回 true并通知条件变量。我们在原始线程中传递了谓词,但我们仍然没有进入SleepConditionVariableSRW
- 调用链很长。
因此,尽管我们通知了条件变量,并且放在条件变量上的谓词肯定会返回 true(因为通知程序是这样做的),但我们仍然阻止条件变量,可能永远。
这是它应该如何表现的吗?这似乎是一个等待发生的丑陋的比赛条件。如果您通知条件变量并且它的谓词返回 true - 线程应该取消阻止。但是,如果我们在检查谓词和睡觉之间处于困境 - 我们将永远被阻止。std::condition_variable::wait
不是原子函数。
标准对此有何规定,它真的是一个竞争条件吗?
你违反了合同,所以所有的赌注都关闭了。请参阅: http://en.cppreference.com/w/cpp/thread/condition_variable
TLDR:当你持有互斥锁时,谓词不可能被其他人更改。
你应该在持有互斥锁的同时更改谓词的基础变量,并且你必须在调用std::condition_variable::wait
之前获取该互斥锁(既是因为wait
释放互斥锁,又因为这是协定)。
在您描述的场景中,更改发生在while (!_Pred())
看到谓词不成立之后,但在wait(_Lck)
有机会释放互斥锁之前。这意味着您更改了谓词在不持有互斥锁的情况下检查的内容。您违反了规则,竞争条件或无限等待仍然不是您可以获得的最糟糕的 UB。至少这些是本地的,并且与您违反的规则相关,因此您可以找到错误......
如果您遵守规则,请:
- 服务员先抓住互斥锁
- 进入
std::condition_variable::wait
.(回想一下,通知程序仍在等待互斥锁。 - 检查谓词并查看它是否成立。(回想一下,通知程序仍在等待互斥锁。
- 调用一些实现定义的魔法来释放互斥锁并等待,直到现在通知程序才能继续。
- 通知者终于设法接受了互斥锁。
- 通知程序更改谓词为真所需的任何更改。
- 通知程序调用
std::condition_variable::notify_one
。
或:
- 通知程序获取互斥锁。(回想一下,服务员在尝试获取互斥锁时被阻止。
- 通知程序更改谓词为真所需的任何更改。(回想一下,服务员仍然被屏蔽。
- 通知程序释放互斥锁。(在途中的某个地方,服务员会打电话给
std::condition_variable::notify_one
,但是一旦互斥锁被释放...... - 服务员获取互斥锁。
- 服务员叫
std::condition_variable::wait
。 - 服务员检查
while (!_Pred())
和中提琴!谓词是真的。 - 服务员甚至不进入内部
wait
,所以通知者是否设法打电话给std::condition_variable::notify_one
或没有设法做到这一点是无关紧要的。
这就是对 cppreference.com 的要求背后的基本原理:
即使共享变量是原子变量,也必须在互斥锁下对其进行修改,以便将修改正确发布到等待线程。
请注意,这是条件变量的一般规则,而不是std::condition_variables
的特殊要求(包括WindowsCONDITION_VARIABLE
s,POSIXpthread_cond_t
s等)。
回想一下,采用谓词的wait
重载只是一个方便的函数,这样调用方就不必处理虚假唤醒。标准 (§30.5.1/15) 明确指出此重载等效于 Microsoft 实现中的 while 循环:
效果:相当于:
while (!pred()) wait(lock);
简单的wait
有效吗?您是否在调用谓词之前和之后测试谓词wait
?伟大。你也在做同样的事情。还是你也在质疑void std::condition_variable::wait( std::unique_lock<std::mutex>& lock );
?
Windows 关键部分和细读/写器锁是用户模式工具而不是内核对象是无关紧要的,与问题无关。有替代实现。如果你有兴趣知道Windows如何设法原子地发布CS/SRWL并进入等待状态(在Vista之前,带有互斥体和事件的天真用户模式实现做错了什么),那就是另一个问题了。
- 并行块(线程清理器)之外的 OpenMP 中的争用条件;误报?
- 如何在C++中创建争用条件
- C++上的手动重置事件(来自 C#)实现:如何避免争用条件
- 作为随机数生成器的争用条件
- 智能指针析构函数争用条件
- 尽管互斥锁,线程中的争用条件
- 在C++中递增和递减全局变量时的争用条件
- 为什么此代码不创建争用条件?
- __has_include() 和后续 #include 之间是否存在争用条件
- 什么保证两个不相关的线程中的不同不相关对象没有(不可避免的)争用条件?
- 此工厂方法是否会导致争用条件?
- 争用条件 2 个线程交替
- 标准::condition_variable 中可能存在的争用条件
- 启动子进程时的争用条件导致从管道读取挂起
- 当只有一个线程写入 c++ 中的布尔变量时,是否存在争用条件
- 避免在增加计数器时出现争用条件
- 是否有 std 或提升容器可以避免其插入和查找方法之间的争用条件
- 增强进程间争用条件预防
- pthread_once() 中的争用条件
- 发布标准实现争用条件