c++ - std::condition_variable 中可能的竞争条件?

标签 c++ multithreading synchronization 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,其中包含旧的好 win32 CONDITION_VARIABLE,并且 wait 调用 __crtSleepConditionVariableSRW

做一些汇编调试,__crtSleepConditionVariableSRW 只是提取 SleepConditionVariableSRW 函数指针,并调用它。

事情是这样的:据我所知,win32 CONDITION_VARIABLE 不是内核对象,而是用户模式对象。因此,如果某个线程通知了这个变量,但实际上没有线程在它上面休眠,那么你就失去了通知,线程将保持休眠状态,直到超时或其他线程通知它。一个小程序实际上可以证明这一点——如果你错过了通知点——你的线程将继续休眠,尽管其他线程通知了它。

我的问题是这样的:
一个线程等待条件变量并且谓词返回 false。然后,发生上面解释的整个调用链。那时,另一个线程改变了环境,因此谓词将返回 true 通知条件变量。我们在原始线程中传递了谓词,但我们仍然没有进入 SleepConditionVariableSRW - 调用链很长。

因此,尽管我们通知了条件变量并且放在条件变量上的谓词肯定会返回 true(因为通知者这样做了),我们仍然阻塞在条件变量上,可能永远阻塞。

这是它的行为方式吗?这似乎是一个巨大的、丑陋的竞争条件等待发生。如果您通知条件变量并且它的谓词返回 true - 线程应该解除阻塞。但是,如果我们在检查谓词和进入休眠之间处于中间状态——我们将永远被阻塞。 std::condition_variable::wait 不是原子函数。

标准是怎么说的?它真的是竞争条件吗?

最佳答案

你违反了契约(Contract),所以所有的赌注都被取消了。请参阅: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())viola! 谓词为真。
  7. 服务员甚至没有进入内部wait ,所以通知者是否成功调用了std::condition_variable::notify_one或者还没有设法做到这一点是无关紧要的。

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

Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread.

请注意,这是对条件变量的一般规则,而不是对 std::condition_variables 的特殊要求s(包括 Windows CONDITION_VARIABLE s、POSIX pthread_cond_t s 等​​)。


回想一下 wait采用谓词的重载只是一个方便的函数,因此调用者不必处理虚假唤醒。标准 (§30.5.1/15) 明确指出此重载等同于 Microsoft 实现中的 while 循环:

Effects: Equivalent to:

while (!pred())
    wait(lock);

做简单的wait工作?你在调用wait之前和之后测试谓词吗? ?伟大的。你也在做同样的事情。或者你在质疑void std::condition_variable::wait( std::unique_lock<std::mutex>& lock );也是吗?


Windows Critical Sections 和 Slim Reader/Writer Locks 是用户模式设施而不是内核对象,这与问题无关紧要。有替代的实现。如果您有兴趣了解 Windows 如何设法以原子方式释放 CS/SRWL 并进入等待状态(使用互斥锁和事件的天真的 pre-Vista 用户模式实现做错了什么),那是另一个问题。

关于c++ - std::condition_variable 中可能的竞争条件?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42470773/

相关文章:

c++ - 三元运算符在递归中的奇怪行为

c++ - GLUT + OpenGL 多重采样抗锯齿 (MSAA)

iphone - 如何在 iPhone 上访问 safari、facebook 等应用程序的远程连接请求

firefox - 如何集成 Firefox 同步?

c++ - 交叉编译 Ubuntu 到 Beaglebone 后无法在目标上找到共享库

c++ - 对文件使用 fopen 时,matlab 在没有转储文件的情况下崩溃

python - Kivy-如何从服务器持续接收数据并使用kivy更新界面

php - 处理高负载文件 I/O 的最佳实践是什么?

go - 如何在实时输出的同时将命令的标准输出和标准错误重定向到控制台和日志文件?

c++ - 这种 boost 条件代码的使用有问题吗?