【问题标题】:std::condition_variable spurious blockingstd::condition_variable 虚假阻塞
【发布时间】:2013-02-10 22:07:31
【问题描述】:

如您所知,应循环调用条件变量以避免虚假唤醒。像这样:

while (not condition)
    condvar.wait();

如果另一个线程想要唤醒等待线程,它必须设置条件标志为真。例如:

condition = true;
condvar.notify_one();

我想知道,条件变量是否可能被这种情况阻止:

1)等待线程检查条件标志,发现它等于FALSE,所以进入condvar.wait()例程。

2)但在此之前(但在条件标志检查之后)等待线程被内核抢占(例如,由于时隙到期)。

3) 这时,另一个线程想要通知等待线程的条件。它将条件标志设置为 TRUE 并调用 condvar.notify_one();

4) 当内核调度器再次运行第一个线程时,它进入condvar.wait() 例程,但已经错过了通知。

因此,尽管条件标志设置为 TRUE,但等待线程无法从 condvar.wait() 退出,因为不再有唤醒通知。

有可能吗?

【问题讨论】:

标签: c++ c++11 condition-variable


【解决方案1】:

这正是条件变量必须与互斥锁结合使用的原因,以便自动更新状态并发出更改信号。完整的代码看起来更像:

unique_lock<mutex> lock(mutex);
while (not condition)
    condvar.wait(lock);

对于另一个线程:

lock_guard<mutex> lock(mutex);
condition = true;
condvar.notify_one();

【讨论】:

  • link - 看不到通知线程锁定互斥锁的位置。此页面是否包含错误?
  • @Vasily 该示例显示了一个设计缺陷,它通过休眠一秒钟来规避。它可能会受到您所描述的问题的影响。在某些情况下,这无关紧要。
  • 是的,我现在明白了。如果通知线程必须有权访问此互斥锁,这是一个解释。谢谢。我以为没有。
  • 我还要提到一个事实,即 C++11 中的类 condition_variable 具有 wait() 成员函数的重载,该函数接受谓词作为其第二个参数,并且在内部实现了循环。
  • 为什么一次std::unique_lock和一次std::lock_guard
【解决方案2】:

你的例子遗漏了一小部分,但这解释了为什么如果做得正确是不可能的:

while (not condition) // when you check condition mutex is locked
    condvar.wait( mutex ); // when you wait mutex is unlocked

所以如果在同一个互斥锁下将condition改为true,就不会出现这种情况了。

【讨论】:

  • 我对此表示怀疑。通知线程对这个互斥锁一无所知,因此,它不能用于访问限制。如果我调用 condvar.notify_one(mutex),那将是正确的。但我没有在通知线程中使用任何互斥锁
  • @Vasily 重点是其他线程无法通知等待的线程,因为它目前在互斥体上被阻塞。
  • 通知线程更改受此互斥锁保护的变量,因此它必须知道互斥锁
  • @Vasily:向 condvar 发出信号的人应该在同一个互斥锁下进行。这完全消除了唤醒/等待竞赛。如果通知线程不知道您的互斥锁,那么它到底在保护什么?
  • @Vasily 我不确定是否必须在互斥锁下调用 notify_one,但更改条件变量(不是 condvar)肯定需要互斥锁,并且不必在文档中。另一个问题是有些人在没有适当锁定的情况下在 intel 平台上更改 bool 或 int,假设它是安全的,但这并不完全正确。
【解决方案3】:

Mike Seymour 他的回答是不完整的,因为有一个竞争条件最终导致唤醒丢失。 正确的方法是(现在用c++11)如下:

线程1:

std::unique_lock<std::mutex> lck(myMutex);
condvar.wait(lck, []{ return condition; }); // prevent spurious wakeup
// Process data

线程2:

{
    std::lock_guard<std::mutex> lck(myMutex);
    condition = true;
} // unlock here! prevent wakeup lost
condvar.notify_one();

【讨论】:

    【解决方案4】:

    是的(我在 2012 年 12 月对此进行了测试),而且我不久前想出了一个解决方案。 “耀斑”类: 请注意,它使用自旋锁,但在此花费的时间很少。

    声明(hpp):

    class Flare
    {
    public:
    /**
    \brief Flare's constructor.
    \param fall_through_first, will skip the first wait() if true.
    */
    Flare(bool fall_through_first = false);
    
    
    /**
    \brief Flare's destructor.
    
    Takes care of removing the object of this class.
    */
    ~Flare();
    
    
    /**
    \brief Notifies the same object of availability.
    
    Any thread waiting on this object will be freed,
    and if the thread was not waiting, it will skip
    wait when it iterates over it.
    */
    void notify();
    
    
    /**
    \brief Wait until the next notification.
    
    If a notification was sent whilst not being
    inside wait, then wait will simply be skipped.
    */
    void wait();
    
    
    private:
        std::mutex m_mx; // Used in the unique_lock,
        std::unique_lock<std::mutex> m_lk; // Used in the cnd_var
        std::condition_variable m_cndvar;
    
        std::mutex m_in_function, n_mx; // protection of re-iteration.
        bool m_notifications;
    
    };
    

    实施/定义 (cpp):

    #include "Flare.hpp"
    
    
    // PUBLIC:
    
    Flare::Flare(bool fall_through_first)
    :
    m_lk(m_mx),
    m_notifications(!fall_through_first)
    {}
    
    Flare::~Flare()
    {}
    
    void Flare::notify()
    {
        if (m_in_function.try_lock() == true)
        {
            m_notifications = false;
            m_in_function.unlock();
        }
        else // Function is waiting.
        {
            n_mx.lock();
            do
            {
                m_notifications = false;
                m_cndvar.notify_one();
            }
            while (m_in_function.try_lock() == false);
            n_mx.unlock();
            m_in_function.unlock();
        }
    }
    
    void Flare::wait()
    {
        m_in_function.lock();
        while (m_notifications)
            m_cndvar.wait(m_lk);
        m_in_function.unlock();
        n_mx.lock();
        m_notifications = true;
        n_mx.unlock();
    }
    

    【讨论】:

    • 那么,您能够重现这种“虚假阻塞”吗?怎么样?
    • 您只需要一个互斥锁,而不是两个。正确使用互斥锁和条件变量(即在同一个互斥锁下设置条件和信号)可确保不会发生这些竞争。
    • 这可以通过让两个线程不断地用原子布尔值通知彼此的 cnd_vars 来避免虚假唤醒来重现。他们会互相关起来。不要忘记,当其他线程尚未进入 wait() 时对 notify_one() 的任何调用都是丢失的通知。哈斯图金介意给我们举个例子吗?每当我类中的通知器完成时,它可能允许服务员多次迭代而不会停止。
    • 看起来你可以通过锁定通知线程的同一个互斥锁来避免这个问题。将所有这些动作封装在一个类中是可能的,但现在总是可取的。
    • @BourgondAries:服务员会这样做lock(mtx); while(!cond) wait(cndvar, mtx); ...; unlock(mtx); 通知程序会这样做lock(mtx); cond = true; signal(cndvar); unlock(mtx);。如果在服务员之前调用通知程序,则服务员永远不会休眠(因为 cond 为真)。不会发生死锁(因为等待 condvar 也会释放互斥锁)。服务员总是醒来,点菜没关系。您也不需要使用原子布尔值(因为您已经有了互斥体)。
    猜你喜欢
    • 1970-01-01
    • 2021-01-02
    • 1970-01-01
    • 2010-10-12
    • 1970-01-01
    • 2019-12-08
    • 2011-06-07
    • 2015-12-22
    • 2018-06-26
    相关资源
    最近更新 更多