为什么std :: condition_variable :: wait需要互斥?

时间:2017-09-07 05:18:42

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

TL; DR

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

答案1

您可以查看文档并引用:

 wait... Atomically releases lock

但这不是一个真正的原因。这只是更加验证我的问题:为什么它首先需要它?

答案2

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

行。公平。 这里有两个问题

  1. 谓词是否始终查询共享资源的状态?我假设是的。我没有意义实施它,否则
  2. 如果我没有传递任何谓词(可选),该怎么办?
  3. 使用谓词 - 锁是有意义的

    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,为什么我们需要锁?

5 个答案:

答案 0 :(得分:3)

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

  1. 确定条件当前不正确。
  2. 它开始等待其他一些线程使条件成立。这是wait电话。
  3. 例如,条件可能是队列中包含元素,并且线程可能会看到队列为空并等待另一个线程将事物放入队列中。

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

    要求保持锁定的目的是防止其他线程像这样进行中断。此外,必须解锁锁以允许其他线程执行我们正在等待的任何操作,但由于等待时通知问题,它不能在wait调用之前发生,并且不会发生在wait电话之后因为我们在等待时无法做任何事情。它必须是wait调用的一部分,因此wait必须知道锁定。

    现在,您可以查看notify_*方法并注意那些方法不需要保持锁定,因此实际上没有任何内容阻止另一个线程在步骤1之间通知然而,调用notify_*的线程应该在执行任何操作时保持锁定以使条件成立,这通常是足够的保护。

答案 1 :(得分:3)

使用std::unique_lock时需要std::condition_variable,原因与使用std::FILE*时需要std::fwrite的原因相同,出于同样的原因,BasicLockable是必要的使用std::unique_lock时。

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

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

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

等等。

如前所述,pred参数是可选的,但是具有某种谓词并且不进行测试。或者,换句话说,没有谓词并没有任何意义,这种方式类似于没有锁定的条件变量没有任何意义。

您拥有锁定的原因是因为您需要保护共享状态以防止同时访问。该共享状态的某些功能是谓词。

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

最后一点是你写的第二个代码片段非常破碎。显然,在您尝试将其作为参数传递给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返回后锁定std::unique_lock的{​​{1}}。

答案 2 :(得分:2)

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

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

  1. 前提条件:消费者代码持有控制消费者条件数据(CCD)的锁(CCL)。
  2. 检查条件,如果为true,则消费者代码中的执行仍继续保持锁定。
  3. 如果为false,它首先获取自己的锁(CVL),将当前线程添加到等待的线程集合中,释放使用者锁定并使其自身等待并释放自己的锁定(CVL)。
  4. 最后一步是棘手的​​,因为它需要睡眠线程并同时或以该顺序释放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<std::mutex>& 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();
    

答案 3 :(得分:0)

<强> TL; DR

  

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

condition_variable旨在等待某个条件成真,而不是等待通知。所以到了#34;赶上&#34; &#34;时刻&#34;当条件成立时,您需要检查条件等待通知。为避免竞争条件,您需要将这两者作为单个原子操作。

condition_variable的目的:

启用程序来实现此目的:当条件 C 成立时,执行一些操作

目标协议:

  • 条件制作者将世界状态从!C 更改为 C
  • 条件使用者等待 C 发生并在 C 条件成立后/之后执行操作。< / LI>

<强>简化:

为简单起见(为了限制要考虑的案例数量),我们假设 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危险)。

因此,如果不锁定使用者,可能会错过条件成为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 :(得分:0)

原因是在某些时候等待线程持有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)