【问题标题】:Using std::conditional_variable to wait on a condition使用 std::conditional_variable 等待条件
【发布时间】:2023-11-02 00:34:01
【问题描述】:

为简单起见,假设我们只有一个条件变量来匹配由布尔值反映的单个条件。

1) 为什么std::condition_variable::wait(...) 在发送“通知”取消睡眠后再次锁定互斥锁?

2) 看到“1)”中的行为,这是否意味着当您执行std::condition_variable::notify_all 时,它只会使所有等待的线程都被解除阻塞/唤醒......但是按顺序 而不是一次全部?如果是这样,有什么办法可以一次性完成?

3) 如果我只关心线程在满足条件之前处于休眠状态,而不关心任何互斥量获取,我该怎么办?是否有替代方案或是否应该围绕这个问题破解当前的std::condition_variable::wait(...) 方法?

如果要使用“hackery”,这个函数是否可以在一个条件下解除阻塞所有等待线程,并且可以从任何(每个线程)线程调用它:

//declared somehwere and modified before sending "notify"(ies)
std::atomic<bool> global_shared_condition_atomic_bool;

//the single(for simplicity in our case) condition variable matched with the above boolean result
std::condition_variable global_shared_condition_variable;

static void MyClass:wait()
{
    std::mutex mutex;
    std::unique_lock<std::mutex> lock(mutex);

    while (!global_shared_condition_atomic_bool) global_shared_condition_variable.wait(lock);
}

它会像这样从随机的​​“等待”线程中调用:

void random_thread_run()
{
    while(someLoopControlValue)
    {
        //random code...
        MyClass:wait(); //wait for whatever condition the class+method is for.
        //more random code...
    }
}

编辑:

门类

#ifndef Gate_Header
#define Gate_Header

#include <mutex>
#include <condition_variable>

class Gate
{
public:
    Gate()
    {
        gate_open = false;
    }

    void open()
    {
        m.lock();
        gate_open = true;
        m.unlock();

        cv.notify_all();
    }

    void wait()
    {
        std::unique_lock<std::mutex> lock(m);

        while (!gate_open) cv.wait(lock);
    }

    void close()
    {
        m.lock();
        gate_open = false;
        m.unlock();
    }

private:
    std::mutex m;
    std::condition_variable cv;
    bool gate_open;
};

#endif

【问题讨论】:

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


    【解决方案1】:

    条件变量会虚假地唤醒事物。

    必须有一个互斥体,并且它必须保护某种类型的消息以使其工作,否则您对发生任何此类唤醒的保证为零。

    这样做大概是因为非虚假版本的有效实现最终会以这种虚假版本的形式实现。

    如果您未能使用互斥锁保护消息编辑(即,没有对其进行同步,则消息的状态是未定义的行为。这可能导致编译器优化从内存读取以在第一次读取后跳过它。

    即使排除了未定义的行为(假设您使用原子),也存在设置消息、发生通知的竞争条件,如果您未能在设置变量和通知条件变量之间的时间。

    除非出现极端情况,否则您通常希望使用 wait 的 lambda 版本。

    除非您同时审核通知代码和等待代码,否则无法审核条件变量代码。

    struct gate {
      bool gate_open = false;
      mutable std::condition_variable cv;
      mutable std::mutex m;
    
      void open_gate() {
        std::unique_lock<std::mutex> lock(m);
        gate_open=true;
        cv.notify_all();
      }
      void wait_at_gate() const {
        std::unique_lock<std::mutex> lock(m);
        cv.wait( lock, [this]{ return gate_open; } );
      }
    };
    

      void open_gate() {
        {
          std::unique_lock<std::mutex> lock(m);
          gate_open=true;
        }
        cv.notify_all();
      }
    

    【讨论】:

    • #1 因此,互斥锁纯粹是为了保护对std::condition_variable 的访问? #2 所以在设置条件布尔值(现在不需要是原子的)并发送通知事件以确保通知作为一个整体进行之前,我总是必须从信号线程获取互斥锁? #3 我还能保留while(!boolVar) 版本吗?我不明白为什么我需要使用更复杂的 2 参数形式,抱歉。
    • @JustinBieber 那是因为你是歌手,而不是程序员。 2-arg 形式更简单且等效;没有流量控制,只是一个测试功能。互斥锁保护对条件变量被测试值的访问。在设置 bool 和发送通知之间的任何时候都没有持有互斥锁,因此开始等待的线程永远不会唤醒。
    • 您能否看一下我作为后期编辑编写的课程,并确认它在生产中使用是安全的?我很抱歉拖了这个,但我很难把头绕过去。这对我们歌手来说很难。
    • @justin 我看不到任何明显的东西,但我永远不会直接lock 锁定类之外的互斥锁。我会使用一个独特的锁和一个范围。关闭锁也是有问题的;对我来说,它看起来像代码味道。
    【解决方案2】:

    不,您的代码将不起作用。

    mutex 保护对共享变量的修改。因此,所有等待线程和信号线程必须锁定特定的mutex 实例。根据您编写的内容,每个线程都有自己的 mutex 实例。

    所有这些mutex 的主要原因是由于spurious wakeup 的概念,这是条件变量的操作系统实现的一个不幸方面。即使条件尚未满足,等待它们的线程有时也会开始运行。

    对实际变量的mutex-bound 检查允许线程测试它是否被虚假唤醒。

    wait 原子地释放mutex 并开始等待条件。当wait 退出时,mutex 作为唤醒过程的一部分被原子地重新获取。现在,考虑虚假唤醒和通知线程之间的竞争。通知线程可以处于以下两种状态之一:即将修改变量,或者修改后即将通知大家醒来。

    如果在通知线程即将修改变量时发生虚假唤醒,则其中一个将首先到达mutex。因此,被虚假唤醒的线程将看到旧值或新值。如果它看到新的,那么它已被通知并会去做它的业务。如果它看到旧的,那么它将再次等待条件。但是如果它看到旧的,那么它阻止通知线程修改那个变量,所以它必须等到虚假线程重新进入睡眠状态。

    为什么 std::condition_variable::wait(...) 在发送“通知”以取消睡眠后再次锁定互斥锁?

    因为mutex 锁定了对条件变量的访问。从wait 呼叫中醒来后,您要做的第一件事就是检查条件变量。因此,这必须在mutex 的保护下进行。

    必须防止信号线程在其他线程正在读取变量时修改变量。这就是mutex 的用途。

    看到“1)”中的行为,这是否意味着当您执行 std::condition_variable::notify_all 时,它只会使所有等待的线程都被解除阻塞/唤醒......但顺序而不是一下子?

    没有指定他们醒来的顺序。但是,当notify_all 返回时,保证所有线程都已解除阻塞。

    如果我只关心线程在满足条件之前休眠,而不关心任何互斥体获取,我该怎么办?

    什么都没有。 condition_variable 要求通过mutex控制对您正在检查的实际变量的访问。

    【讨论】: