在调用condition_variable.notify_one()之前是否必须获得锁定?


90

我对使用有点困惑std::condition_variable。我了解我必须在致电之前创建一个unique_lockon 。我找不到的是在致电或之前是否还应该获得一个唯一的锁。mutexcondition_variable.wait()notify_one()notify_all()

cppreference.com上的示例存在冲突。例如,notify_one页面给出了以下示例:

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying...\n";
    cv.notify_one();

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
        std::cerr << "Notifying again...\n";
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); t2.join();
}

在这里,锁不是为第一个获取的notify_one(),而是为第二个获取的notify_one()。翻看其他带有示例的页面,我看到了不同的事情,大多数情况下没有获得锁定。

  • 我可以在呼叫之前选择自己锁定互斥锁notify_one(),为什么选择锁定互斥锁?
  • 在给定的示例中,为什么first锁没有锁notify_one(),但随后的调用却锁。这个例子是错误的还是有一定的道理?

Answers:


77

调用时不需要保持锁condition_variable::notify_one(),但是从它仍然是定义良好的行为而不是错误的意义上来说,这并没有错。

但是,这可能是“悲观的”,因为无论等待线程变为可运行的(如果有的话)都将立即尝试获取通知线程持有的锁。我认为避免在调用notify_one()或时持有与条件变量关联的锁是一个很好的经验法则notify_all()。有关示例,请参见Pthread Mutex:pthread_mutex_unlock()消耗大量时间,其中在调用notify_one()可衡量的改进性能的pthread等效项之前释放锁。

请记住,在某些时候必须在循环中进行lock()调用while,因为在while (!done)循环条件检查期间需要保持锁定。但呼叫不需要保留notify_one()


2016年2月27日:进行了较大的更新,以解决评论中是否存在比赛条件是否是锁的问题,这对notify_one()通话没有帮助。我知道此更新很晚,因为这个问题是在大约两年前提出的,但是如果生产者(signals()在此示例中)notify_one()在消费者(waits()在此示例中)之前打电话给我,我想解决@Cookie关于可能的竞争条件的问题。能够打电话wait()

关键是发生了什么i-这实际上是指示消费者是否有“工作”要做的对象。该condition_variable只是为了让消费者有效地等待变化的机制i

生产者在更新时需要持有该锁i,而使用者在检查i和调用时必须持有该锁condition_variable::wait()(如果需要等待的话)。在这种情况下,关键在于,当使用者执行此检查并等待时,它必须是持有锁的同一实例(通常称为关键部分)。由于关键部分在生产者更新i以及消费者检查并等待时保持i,因此没有机会i在消费者检查i和呼叫之间进行切换condition_variable::wait()。这是正确使用条件变量的关键。

C ++标准说,用谓词调用condition_variable :: wait()的行为类似于以下内容(在这种情况下):

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

消费者检查时可能会发生两种情况i

  • 如果i为0,则消费者调用cv.wait(),然后在调用实现iwait(lock)一部分时仍为0-正确使用锁可以确保这一点。在这种情况下,直到使用者调用之后,生产者才有机会condition_variable::notify_one()在其while循环中调用in cv.wait(lk, []{return i == 1;})(并且该wait()调用已完成了其正确“捕获”通知所需的所有操作-wait()在完成该操作之前不会释放该锁。 )。因此,在这种情况下,消费者不能错过通知。

  • 如果i使用者调用时if已经为1 cv.wait()wait(lock)则将永远不会调用实现的一部分,因为while (!pred())测试将导致内部循环终止。在这种情况下,何时调用notify_one()并不重要-使用者不会阻塞。

此处的示例确实具有额外的复杂性,即使用done变量向生产者线程发信号,表明消费者已认识到这一点i == 1,但是我认为这根本不会改变分析,因为所有的访问done(用于读取和修改) )是在与和相同的关键部分中完成icondition_variable

如果您查看@ eh9指出的问题,即使用std :: atomic和std :: condition_variable进行同步是不可靠的则会看到竞争条件。但是,该问题中发布的代码违反了使用条件变量的基本规则之一:执行检查和等待时,它不包含单个关键部分。

在该示例中,代码如下所示:

if (--f->counter == 0)      // (1)
    // we have zeroed this fence's counter, wake up everyone that waits
    f->resume.notify_all(); // (2)
else
{
    unique_lock<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

您会注意到wait()在按住的同时执行了#3 f->resume_mutex。但是,在完全保持该锁的同时,wait()没有完成步骤1中是否需要进行检查(检查和等待的连续性要差得多),这是正确使用条件变量的要求。我相信对该代码段有问题的人认为,既然f->counter是这种std::atomic类型,就可以满足要求。但是,提供的原子性std::atomic不会扩展到对的后续调用f->resume.wait(lock)。在此示例中,在何时f->counter检查(步骤#1)和wait()调用何时(步骤#3)之间存在竞争。

在该问题的示例中不存在该种族。


2
它具有更深的含义:domaigne.com/blog/computing/…值得注意的是,您提到的pthread问题应该通过更新的版本或带有正确标志的版本来解决。(启用wait morphing优化)此链接中解释的经验法则:在具有2个以上线程的情况下,使用WITH锁通知更好,以获得更可预测的结果。
v.oddou 2014年

6
@Michael:据我了解,消费者最终需要致电the_condition_variable.wait(lock);。如果不需要同步生产者和使用者的锁(例如,底层是无锁的spsc队列),那么如果生产者不锁定该锁,则该锁将无用。我没意见。但是难道没有罕见比赛的风险吗?如果生产者不持有该锁,那么他不能在消费者就在等待之前打电话给notify_one吗?然后,消费者需要等待并且不会醒来……
Cookie

1
例如,在上面的代码中说,消费者在std::cout << "Waiting... \n";生产者处在,而cv.notify_one();唤醒者不在,那么唤醒呼叫就丢失了……还是我在这里丢失了一些东西?
Cookie的

1
@曲奇饼。是的,那里有比赛条件。见stackoverflow.com/questions/20982270/...
EH9

1
@ eh9:该死的,由于您的评论,我不时发现了导致代码冻结的错误原因。这是由于这种种族情况的确切情况。通知完全解锁后,互斥体解锁即可解决问题。非常感谢!
galinette '16

10

情况

使用vc10和Boost 1.56,我实现了一个并发队列,就像这篇博客所建议的那样。作者将互斥锁解锁以最大程度地减少争用,即notify_one()在互斥锁未锁定的情况下被调用:

void push(const T& item)
{
  std::unique_lock<std::mutex> mlock(mutex_);
  queue_.push(item);
  mlock.unlock();     // unlock before notificiation to minimize mutex contention
  cond_.notify_one(); // notify one waiting thread
}

Boost文档中的示例支持解锁互斥锁:

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

问题

但这仍然导致以下不稳定的行为:

  • notify_one()一直没有被调用但cond_.wait()仍然可以通过中断boost::thread::interrupt()
  • 曾经notify_one()第一次被称为cond_.wait()僵局;等待不能以boost::thread::interrupt()或结束boost::condition_variable::notify_*()

删除该行mlock.unlock()可使代码按预期工作(通知和中断结束等待)。请注意,notify_one()在互斥锁仍处于锁定状态下调用该函数,然后在离开示波器时立即将其解锁:

void push(const T& item)
{
  std::lock_guard<std::mutex> mlock(mutex_);
  queue_.push(item);
  cond_.notify_one(); // notify one waiting thread
}

这意味着,至少在我特定的线程实现中,互斥体在调用之前一定不能解锁boost::condition_variable::notify_one(),尽管两种方法似乎都是正确的。


您是否向Boost.Thread报告了此问题?我找不到类似的任务有svn.boost.org/trac/boost/...
magras

@magras可悲的是我没有,不知道为什么我没有考虑这一点。不幸的是,我无法使用提到的队列成功重现此错误。
马特乌斯·布兰德

我不确定我是否知道过早唤醒会导致死锁。具体来说,如果您在push()释放队列互斥体之后但在调用notify_one()之前从pop()的cond_.wait()中出来-Pop()应该看到队列为非空,并使用新条目而不是使用等候。如果在push()更新队列时从cond_.wait()中退出,则锁应由push()持有,因此pop()应该阻止等待释放锁。任何其他早期唤醒都将保持锁定状态,以防止push()在pop()调用下一个wait()之前修改队列。我错过了什么?
凯文

4

正如其他人所指出的,notify_one()就竞争条件和与线程相关的问题而言,在调用时,您无需保持锁定状态。但是,在某些情况下,可能需要握住锁以防止在调用condition_variable之前将其销毁notify_one()。考虑以下示例:

thread t;

void foo() {
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    t = std::thread([&]() {
        {
            std::lock_guard<std::mutex> l(m);  // (1)
            done = true;  // (2)
        }  // (3)
        cv.notify_one();  // (4)
    });  // (5)

    std::unique_lock<std::mutex> lock(m);  // (6)
    cv.wait(lock, [&done]() { return done; });  // (7)
}

void main() {
    foo();  // (8)
    t.join();  // (9)
}

假设在创建线程t之后但在开始等待条件变量之前(在(5)和(6)之间的某个位置),有一个上下文切换到新创建的线程。线程t获取锁(1),设置谓词变量(2),然后释放锁(3)。假设在notify_one()执行(4)之前,此时还有另一个上下文切换。主线程获取锁(6)并执行第(7)行,这时谓词返回true且没有理由等待,因此释放了锁并继续。foo返回(8),其范围内的变量(包括cv)将被销毁。在线程t可以加入主线程(9)之前,它必须完成执行,因此它从中断处继续执行cv.notify_one()(4),此时cv已被破坏!

在这种情况下,可能的解决方法是在调用时保持按住该锁notify_one(即,删除以(3)行结尾的范围)。这样,我们确保之前的线程t调用可以检查新设置的谓词变量并继续执行,因为它将需要获取 当前持有的锁来进行检查。因此,我们确保返回后不会被线程访问。notify_onecv.waittcvtfoo

总而言之,在这种特定情况下,问题实际上与线程无关,而是与通过引用捕获的变量的生存期有关。cv是通过thread引用捕获的t,因此您必须确保cv在线程执行期间保持活动状态。此处介绍的其他示例不会遇到此问题,因为condition_variablemutex对象是在全局范围内定义的,因此可以保证它们保持活动状态,直到程序退出。


1

@Michael Burr是正确的。condition_variable::notify_one不需要锁定变量。如示例所示,没有什么可以阻止您在这种情况下使用锁。

在给定的示例中,锁是由并发使用变量驱动的i。由于signals线程修改了变量,因此需要确保在此期间没有其他线程访问该变量。

锁用于需要同步的任何情况,我认为我们不能以更一般的方式声明它。


当然,但是最重要的是,它们还需要与条件变量一起使用,以便整个模式真正起作用。值得注意的是,条件变量wait函数正在释放调用内部的锁,并且仅在重新获取锁之后才返回。在此之后,您可以安全地检查自己的状况,因为您已经获得了“阅读权”。如果仍然没有等待,请返回wait。这就是模式。顺便说一句,这个例子不尊重它。
v.oddou 2014年

1

在某些情况下,当cv可能被其他线程占用(锁定)时。您需要先获得锁定并释放它,然后再通知_()。
如果不是,那么notify _ *()可能根本不执行。


1

仅添加此答案是因为我认为已接受的答案可能会误导您。在所有情况下,您都需要先锁定互斥锁,然后才能在某个地方调用notify_one()来使代码具有线程安全性,尽管您可以在实际调用notify _ *()之前再次对其进行解锁。

为了明确起见,您必须在输入wait(lk)之前先获取该锁,因为wait()会解锁lk,如果未锁定该锁,则它将是未定义行为。notify_one()并非如此,但是您需要确保在进入wait()让该调用解锁互斥锁之前不会调用notify _ *()。显然,这只能通过在调用notify _ *()之前锁定相同的互斥锁来完成。

例如,考虑以下情况:

std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
    cv.notify_one();
}

bool start()
{
  if (count.fetch_add(1) >= 0)
    return true;
  // Failure.
  stop();
  return false;
}

void cancel()
{
  if (count.fetch_sub(1000) == 0)  // Reached -1000?
    return;
  // Wait till count reached -1000.
  std::unique_lock<std::mutex> lk(cancel_mutex);
  cancel_cv.wait(lk);
}

警告:此代码包含错误。

这个想法是这样的:线程成对调用start()和stop(),但前提是start()返回true。例如:

if (start())
{
  // Do stuff
  stop();
}

一个(其他)线程在某个时候将调用cancel(),并且从cancel()返回之后,将销毁“执行任务”中所需的对象。但是,如果在start()和stop()之间有线程,则cancel()应该不会返回,并且一旦cancel()执行了第一行,start()将始终返回false,因此没有新线程会输入'Do东西的区域。

有效吗?

理由如下:

1)如果任何线程成功执行了start()的第一行(并因此将返回true),那么还没有线程执行过cancel()的第一行(我们假设线程总数远小于1000。方式)。

2)此外,虽然一个线程成功执行了start()的第一行,但尚未成功执行stop()的第一行,但是任何线程都不可能成功执行cancel()的第一行(请注意,只有一个线程曾经调用cancel()):fetch_sub(1000)返回的值将大于0。

3)一旦线程执行了cancel()的第一行,start()的第一行将始终返回false,并且调用start()的线程将不再进入“填充”区域。

4)对start()和stop()的调用次数始终是平衡的,因此,在成功执行cancel()的第一行后,总会有片刻对stop()的调用(最后一次)引起计数达到-1000,因此将调用notify_one()。请注意,只有在取消的第一行导致该线程掉线时,才会发生这种情况。

除了饥饿问题,其中有很多线程正在调用start()/ stop(),计数永远不会达到-1000,cancel()永远不会返回,这可能会被人们认为是“不太可能且永远不会持续很长时间”,还有另一个错误:

“填充”区域内可能有一个线程,可以说它只是调用stop();。此时,线程执行cancel()的第一行,并使用fetch_sub(1000)读取值1并掉落。但是在获取互斥体和/或调用wait(lk)之前,第一个线程执行stop()的第一行,读取-999并调用cv.notify_one()!

然后在对条件变量进行wait()之前,完成对notify_one()的调用!该程序将无限期陷入僵局。

因此,在调用wait()之前,我们应该不能调用notify_one ()。请注意,条件变量的功能在于可以自动解锁互斥锁,检查是否发生了对notify_one()的调用并进入睡眠状态。您不能愚弄它,但是每当您对可能将条件从false更改为true的变量进行更改时,必须保持互斥锁处于锁定状态,并由于此处所述的竞争条件而在调用notify_one()时保持锁定。

但是,在此示例中,没有条件。为什么我不使用'count == -1000'作为条件?因为这根本没什么意思:一旦达到-1000,我们就确定没有新线程会进入“处理任务”区域。此外,线程仍可以调用start()并将计数增加(至-999和-998等),但我们对此并不在意。唯一重要的是达到了-1000,因此我们可以肯定地知道“工作”区域中不再有线程。我们确定在调用notify_one()时确实是这种情况,但是如何确保在cancel()锁定其互斥锁之前不调用notify_one()呢?当然,仅在notify_one()之前不久就锁定cancel_mutex毫无帮助。

问题是,尽管我们不是等待状态,还有就是一个条件,我们需要锁定互斥

1)在达到该条件之前2)在我们调用notify_one之前。

因此,正确的代码变为:

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
  {
    cancel_mutex.lock();
    cancel_mutex.unlock();
    cv.notify_one();
  }
}

[...相同的start()...]

void cancel()
{
  std::unique_lock<std::mutex> lk(cancel_mutex);
  if (count.fetch_sub(1000) == 0)
    return;
  cancel_cv.wait(lk);
}

当然,这只是一个例子,但其他情况非常相似。在几乎所有使用条件变量的情况下,都需要在调用notify_one()之前(不久)锁定该互斥锁,否则有可能在调用wait()之前调用它。

请注意,在这种情况下,我在调用notify_one()之前先解锁了互斥锁,因为否则,对notify_one()的调用很有可能会唤醒线程,等待条件变量,然后该线程将尝试获取互斥锁并块,然后再次释放互斥锁。那只是比需要的慢一点。

此示例有点特殊,因为更改条件的行由调用wait()的同一线程执行。

更常见的情况是,一个线程只是在等待条件变为真,而另一个线程在更改该条件所涉及的变量(导致它可能变为真)之前获取了锁。在那种情况下,互斥锁在条件变为真之前(和之后)立即锁定-因此在这种情况下,在调用notify _ *()之前解锁互斥锁是完全可以的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.