【问题标题】:Possible race condition in std::condition_variable?std::condition_variable 中可能的竞争条件?
【发布时间】:2017-07-17 04:22:30
【问题描述】:

我研究了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-&gt;_get_cv()-&gt;wait(cs);(所有这些都在文件cond.c 中)。

cond-&gt;_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 - 线程应该解除阻塞。但是,如果我们在检查谓词和睡觉之间处于两难境地——我们将永远被阻塞。 std::condition_variable::wait 不是原子函数。

标准对此有何规定?它真的是一种竞争条件吗?

【问题讨论】:

    标签: c++ multithreading synchronization condition-variable


    【解决方案1】:

    你违反了合同,所以所有赌注都被取消了。见: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 要求背后的基本原理:

    即使共享变量是原子的,也必须在互斥体下进行修改,才能正确地将修改发布到等待线程。

    请注意,这是条件变量的一般规则,而不是 std::condition_variabless 的特殊要求(包括 Windows CONDITION_VARIABLEs、POSIX pthread_cond_ts 等)。


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

    效果:相当于:

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

    简单的wait 有效吗?你在调用wait 之前和之后测试谓词吗?伟大的。你也在做同样的事情。还是你也在质疑void std::condition_variable::wait( std::unique_lock&lt;std::mutex&gt;&amp; lock );


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

    【讨论】:

    • aha,我错过了“即使共享变量是原子的......”这是我的想法。
    • @DavidHaim:请注意,这是 cppreference 给出的提示。该标准没有说明任何内容。 std::condition_variable(以及一般的条件变量)在等待和通知之间创建关系并持有互斥锁。假设您有正当理由以这种方式获取互斥锁。或不。如何处理该互斥体是您的事。条件变量真正做的唯一一件事就是释放互斥体并原子地进入等待状态。 “条件”部分完全取决于您。
    猜你喜欢
    • 1970-01-01
    • 2013-04-13
    • 2021-12-06
    • 1970-01-01
    • 2019-09-09
    • 1970-01-01
    • 2015-04-18
    • 2013-05-06
    • 2019-11-25
    相关资源
    最近更新 更多