【问题标题】:why does std::condition_variable::wait need mutex?为什么 std::condition_variable::wait 需要互斥锁?
【发布时间】:2018-02-15 17:17:39
【问题描述】:

TL;DR

为什么std::condition_variable::wait 需要一个互斥锁作为其变量之一?


回答 1

您可以查看文档并引用:

 wait... Atomically releases lock

但这不是真正的原因。这进一步验证了我的问题:为什么它首先需要它?

回答 2

谓词最有可能查询共享资源的状态,它必须被锁保护。

好的。公平的。 这里有两个问题

  1. 谓词查询共享资源的状态是否总是正确的?我认为是的。否则我没有意义去实现它
  2. 如果我不传递任何谓词(它是可选的)怎么办?

使用谓词 - 锁定有意义

int i = 0;
void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    cv.wait(lk, []{return i == 1;});
    std::cout << i;
}

不使用谓词 - 为什么我们不能在等待后锁定?

int i = 0;
void waits()
{
    cv.wait(lk);
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << i;
}

注意事项

我知道这种做法没有有害影响。我只是不知道如何向自己解释为什么会这样设计?

问题

如果谓词是可选的并且没有传递给wait,为什么我们需要锁?

【问题讨论】:

  • 如果您不将谓词传递给wait,则必须在对wait 的调用返回后自己检查谓词。你总是需要一些谓词,没有例外。唤醒本身是没有意义的(查找“虚假唤醒”)。
  • 另见 this(描述 POSIX condvars,但 C++ 仿照 POSIX)。
  • 是的。我必须自己检查谓词但是我可以自己锁定互斥锁 wait
  • 总是需要在等待后立即锁定互斥锁。目前尚不清楚您为什么要将这些操作分开。
  • I just don't know how to explain to my self why it was design this way? 选择您自己的设计,然后尝试使用它。例如,尝试实现 FIFO 队列,pop() 操作等待空队列。很可能,您将无法使用您的设计中的wait 来实现这一点。但是: 1. C++11 的等待方法并不是唯一可能的方法。一些库提供了另一种方法。 2.您在“不使用谓词”下的示例实际上不需要锁定互斥锁。但是这个例子本身并不是很有用。

标签: multithreading c++11 mutex wait condition-variable


【解决方案1】:

当使用条件变量等待条件时,线程执行以下步骤序列:

  1. 它确定条件当前不为真。
  2. 它开始等待某个其他线程使条件为真。这是wait 电话。

例如,条件可能是队列中有元素,线程可能看到队列为空并等待另一个线程将内容放入队列中。

如果另一个线程在这两个步骤之间进行调解,它可以使条件为真并在第一个线程真正开始等待之前通知条件变量。在这种情况下,等待的线程不会收到通知,它可能永远不会停止等待。

要求持有锁的目的是为了防止其他线程这样插手。此外,锁必须被解锁以允许其他线程执行我们正在等待的任何事情,但由于 notify-before-wait 问题,它不能在 wait 调用之前发生,并且在wait 打电话,因为我们在等待的时候什么也做不了。它必须是wait 调用的一部分,所以wait 必须知道锁。

现在,您可能会查看 notify_* 方法并注意到 那些 方法不需要持有锁,因此实际上并没有阻止另一个线程在步骤 1 和 2 之间进行通知. 但是,调用notify_* 的线程应该在执行任何操作以使条件为真时持有锁,这通常是足够的保护。

【讨论】:

  • 原子释放锁这一步与你提到的两个步骤在哪里吻合?
  • @idanshmu:这是第 2 步的一部分。
  • 谓词为空怎么办?在这种情况下,锁是多余的吗?
  • @idanshmu:就像 n.m.说,总有一个谓词,无论你是否将一个传递给wait。你在等待某事
  • 但是我们不是说等待在之后锁被释放了吗?
【解决方案2】:

使用std::condition_variable 时需要std::unique_lock,原因与使用std::fwrite 时需要std::FILE* 相同,使用std::unique_lock 本身时也需要BasicLockable

std::fwrite 功能为您提供了它存在的全部原因,就是写入文件。所以你必须给它一个文件。 std::unique_lock 为您提供的功能是 RAII 锁定和解锁互斥体(或另一个 BasicLockable,如 std::shared_mutex 等),因此您必须为其提供锁定和解锁的东西。

std::condition_variable 提供的功能,它存在的全部原因,是原子地等待和解锁锁(并完成等待和锁定)。所以你必须给它一些东西来锁定。


为什么有人想要这是一个已经讨论过的单独问题。例如:

等等。


正如已经解释的那样,pred 参数是可选的,但具有某种谓词并对其进行测试不是。或者,换句话说,没有谓词没有任何意义,就像没有锁的条件变量没有任何意义一样。

你有一个锁的原因是你有共享状态,你需要防止同时访问。该共享状态的某些功能谓词。

如果你没有谓词也没有锁,你真的不需要条件变量,就像你没有文件你真的不需要fwrite一样。


最后一点是,您编写的第二个代码 sn-p 非常损坏。显然,在您尝试将锁作为参数传递给condition_variable::wait() 之后,它不会在您定义锁时编译。你的意思可能是这样的:

std::mutex mtx_cv;
std::condition_variable cv;

...

{
    std::unique_lock<std::mutex> lk(mtx_cv);
    cv.wait(lk);
    lk.lock();    // throws std::system_error with an error code of std::errc::resource_deadlock_would_occur
}

这是错误的原因很简单。 condition_variable::wait 的效果是(来自[thread.condition.condvar]):

效果:
— 原子地调用 lock.unlock() 并阻塞 *this。
— 解除阻塞时,调用 lock.lock()(可能在锁上阻塞),然后返回
— 当调用 notify_one() 或调用 notify_all() 或虚假发出信号时,该函数将解除阻塞

wait() 返回后,锁被锁定,如果unique_lock::lock() 已经锁定了它所包裹的互斥体 ([thread.lock.unique.locking]),则会引发异常。

再次,为什么有人想要像std::condition_variable那样进行耦合等待和锁定是一个单独的问题,但鉴于它确实 - 你不能,根据定义,在std::condition_variable::wait 返回后锁定std::condition_variablestd::unique_lock

【讨论】:

    【解决方案3】:

    TL;DR

    如果谓词是可选的并且没有传递给等待,为什么我们需要锁?

    condition_variable 旨在等待某个条件实现,而不是仅仅等待通知。因此,要在条件变为真时“捕捉”“时刻”,您需要检查条件等待通知。并且为了避免竞争条件,您需要将这两个操作作为单个原子操作。

    condition_variable 的用途:

    启用程序来实现这一点:当条件 C 成立时执行一些动作

    预期协议:

    • 条件 producer 将世界的状态从 !C 更改为 C
    • 条件消费者等待C发生并在C条件成立时/之后采取动作

    简化:

    为简单起见(为了限制要考虑的案例数量),我们假设 C 永远不会切换回 !C。让我们也忘记虚假唤醒。即使有了这个假设,我们也会看到锁是必要的。

    天真的方法:

    让我们有两个线程,其中的基本代码总结如下:

    void producer() {
      _condition = true;
      _condition_variable.notify_all();
    }
    
    void consumer() {
      if (!_condition) {
        _condition_variable.wait();
      }
      action();
    }
    

    问题:

    这里的问题是race condition。以下是有问题的线程交错:

    • 消费者读取条件,检查它是false并决定等待。
    • 线程调度程序中断消费者并恢复生产者
    • 生产者条件更新为true并调用notify_all()
    • 消费者已恢复。
    • 消费者实际上做了wait(),但从未被通知和唤醒liveness 危险)。

    因此,如果不锁定 consumer 可能会错过 condition 变为 true 的事件。

    解决方案:

    免责声明:此代码仍然无法处理虚假唤醒和条件再次变为false 的可能性。

    void producer() {
      { std::unique_lock<std::mutex> l(_mutex);
        _condition = true;
      }
      _condition_variable.notify_all();
    }
    
    void consumer() {
      { std::unique_lock<std::mutex> l(_mutex);
        if (!_condition) {
          _condition_variable.wait(l);
        }
      }
      action();
    }
    

    这里我们检查条件,释放锁并开始等待作为单个原子操作,防止前面提到的竞争条件。

    另见

    Why Lock condition await must hold the lock

    【讨论】:

      【解决方案4】:

      文档中没有说明(并且可以以不同的方式实现),但从概念上讲,您可以想象条件变量有另一个互斥锁,既可以保护自己的数据,又可以通过修改消费者代码数据来协调条件、等待和通知(例如queue.size()) 影响测试。

      因此,当您致电 wait(...) 时(逻辑上)会发生以下情况。

      1. 前提条件:消费者代码持有控制消费者状况数据 (CCD) 的锁 (CCL)。
      2. 检查条件,如果为真,消费者代码中的执行将继续持有锁。
      3. 如果为false,则先获取自己的锁(CVL),将当前线程加入等待线程集合,释放消费者锁,将自己置于等待状态,释放自己的锁(CVL)。

      最后一步很棘手,因为它需要同时休眠线程并释放 CVL,或者以该顺序或以在等待之前通知的线程能够(以某种方式)不去等待的方式。

      在释放 CCD 之前获取 CVL 的步骤是关键。任何尝试更新 CCD 和通知的并行线程都将被 CCL 或 CVL 阻塞。如果在获取 CVL 之前释放了 CCL,则并行线程可以获取 CCL,更改数据,然后在待等待线程添加到等待者之前通知。

      并行线程获取 CCL,修改数据以使条件为真(或至少值得测试),然后通知。通知获取 CVL 并标识一个阻塞的线程(或多个线程)(如果有)以取消等待。未等待的线程随后会寻求获取 CCL 并可能会阻塞在那里,但不会离开等待并重新执行测试,直到它们获得它。

      通知必须获取 CVL 以确保发现测试错误的线程已添加到等待者。

      在不持有 CCL 的情况下进行通知是可以的(对于性能而言可能更可取),因为等待代码中 CCL 和 CVL 之间的切换确保了排序。 这可能是更可取的,因为在持有 CCL 时通知可能意味着所有未等待的线程只是等待阻塞(在 CCL 上),而修改数据的线程仍持有锁。

      请注意,即使 CCD 是原子的,您也必须修改它以持有 CCL 或 Lock CVL,解锁 CCL 步骤不会确保确保在线程正在进行时不发送通知所需的总排序等待。

      该标准仅讨论操作的原子性,而另一种实现可能会在测试失败后完成“添加到服务员”步骤之前阻止通知。 C++ 标准小心翼翼地不规定实现。

      总之,回答一些具体问题。

      必须共享状态吗? 有点。可能存在外部条件,例如目录中的文件,并且等待在一段时间后重试。您可以自行决定是否将文件系统甚至挂钟视为共享状态。

      一定要有状态吗?不一定。线程可以等待通知。 这可能很难协调,因为必须有足够的顺序来阻止另一个线程不按顺序通知。最常见的解决方案是让通知线程设置一些布尔标志,以便通知线程知道它是否错过了它。 void wait(std::unique_lock&lt;std::mutex&gt;&amp; lk) 的正常使用是在外部检查谓词时:

      std::unique_lock<std::mutex> ulk(ccd_mutex)
      while(!condition){
          cv.wait(ulk);
      }
      

      通知线程使用的地方:

      {
          std::lock_guard<std::mutex> guard(ccd_mutex);
          condition=true;
      }
      cv.notify();
      

      【讨论】:

        【解决方案5】:

        原因是在某些时候等待线程持有m_mutex

        #include <mutex>
        #include <condition_variable>
        
        void CMyClass::MyFunc()
        {
            std::unique_lock<std::mutex> guard(m_mutex); 
        
            // do something (on the protected resource)
        
            m_condiotion.wait(guard, [this]() {return !m_bSpuriousWake; });
        
            // do something else (on the protected resource)
        
            guard.unluck();
        
            // do something else than else
        }
        

        在持有m_mutex 时,线程不应该进入睡眠状态。一个人不想在睡觉时把每个人都锁在外面。因此,原子地:{guard 已解锁,线程进入睡眠状态}。一旦它被另一个线程唤醒(比方说m_condiotion.notify_one()guard 再次被锁定,然后线程继续。


        Reference (video)

        【讨论】:

          【解决方案6】:

          因为如果不是这样,在等待线程注意到共享状态的变化和 wait() 调用之前会有一个竞争条件。 假设我们有一个 std::atomic state_ 类型的共享状态,等待线程仍有可能错过通知:

                  T1(waiting)                            |       T2(notification)
          ---------------------------------------------- * ---------------------------
          1) for (int i = state_; i != 0; i = state_) {  |
          2)                                             |     state_ = 0;
          3)                                             |     cv.notify();
          4)    cv.wait();                               |     
          5) }
          6) // go on with the satisfied condition...    |    
          

          请注意,wait() 调用未能注意到 state_ 的最新值,可能会一直等待。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-09-07
            • 2021-06-04
            • 1970-01-01
            • 2019-11-24
            • 2018-03-01
            • 2022-10-08
            • 1970-01-01
            • 2012-02-23
            相关资源
            最近更新 更多