标准::condition_variable 中可能存在的争用条件

Possible race condition in std::condition_variable?

本文关键字:存在 争用条件 condition variable 标准      更新时间:2023-10-16

我研究了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。至少这些是本地的,并且与您违反的规则相关,因此您可以找到错误......

如果您遵守规则,请:

  1. 服务员先抓住互斥锁
  2. 进入std::condition_variable::wait.(回想一下,通知程序仍在等待互斥锁。
  3. 检查谓词并查看它是否成立。(回想一下,通知程序仍在等待互斥锁。
  4. 调用一些实现定义的魔法来释放互斥锁并等待,直到现在通知程序才能继续。
  5. 通知者终于设法接受了互斥锁。
  6. 通知程序更改谓词为真所需的任何更改。
  7. 通知程序调用std::condition_variable::notify_one

或:

  1. 通知程序获取互斥锁。(回想一下,服务员在尝试获取互斥锁时被阻止。
  2. 通知程序更改谓词为真所需的任何更改。(回想一下,服务员仍然被屏蔽。
  3. 通知程序释放互斥锁。(在途中的某个地方,服务员会打电话给std::condition_variable::notify_one,但是一旦互斥锁被释放......
  4. 服务员获取互斥锁。
  5. 服务员叫std::condition_variable::wait
  6. 服务员检查while (!_Pred())中提琴!谓词是真的。
  7. 服务员甚至不进入内部wait,所以通知者是否设法打电话给std::condition_variable::notify_one或没有设法做到这一点是无关紧要的。

这就是对 cppreference.com 的要求背后的基本原理:

即使共享变量是原子变量,也必须在互斥锁下对其进行修改,以便将修改正确发布到等待线程。

请注意,这是条件变量的一般规则,而不是std::condition_variables的特殊要求(包括WindowsCONDITION_VARIABLEs,POSIXpthread_cond_ts等)。


回想一下,采用谓词的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之前,带有互斥体和事件的天真用户模式实现做错了什么),那就是另一个问题了。