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

时间:2013-06-14 05:51:50

标签: c++ multithreading condition-variable

我对使用std::condition_variable感到有点困惑。我知道在调用unique_lock之前,我必须在mutex上创建condition_variable.wait()。我无法找到的是在致电notify_one()notify_all()之前是否还应该获得一个独特的锁。

cppreference.com上的示例存在冲突。例如,notify_one page给出了这个例子:

#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()之前,我可以选择自己锁定互斥锁吗?为什么我会选择锁定它?
  • 在给出的示例中,为什么第一个notify_one()没有锁定,但后续调用有效。这个例子是错的还是有一些理由?

6 个答案:

答案 0 :(得分:56)

在致电condition_variable::notify_one()时,您无需持锁,但从某种意义上说,它仍然是明确定义的行为,而不是错误。

但是,它可能是一个悲观的&#34;因为任何等待线程都是可运行的(如果有的话)将立即尝试获取通知线程所持有的锁。我认为在调用notify_one()notify_all()时避免持有与条件变量关联的锁是一个很好的经验法则。有关释放锁定的示例,请参阅Pthread Mutex: pthread_mutex_unlock() consumes lots of time,然后在调用相当于notify_one()的pthread时,可以显着提高性能。

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


2016-02-27 :大量更新,以解决有关竞争状况是否锁定的评论中的一些问题对notify_one()的帮助不起作用呼叫。我知道这个更新已经晚了,因为这个问题差不多是在两年前提出来的,但是如果生产者(本例中为signals()),我想解决@Cookie关于可能的竞争条件的问题。在消费者(本例中为notify_one())之前调用waits()就可以调用wait()

关键是i发生的事情 - 实际上表明消费者是否有工作的对象&#34;去做。 condition_variable只是让消费者有效等待更改为i的机制。

生产者需要在更新i时保持锁定,并且消费者必须在检查i并调用condition_variable::wait()时保持锁定(如果需要等待)。在这种情况下,关键是当消费者执行此检查和等待时,它必须是持有锁(通常称为临界区)的相同实例。由于关键部分是在生产者更新i时以及当消费者在i上检查并等待时保留的,因此i没有机会在消费者检查i之间进行更改1}}以及何时调用condition_variable::wait()。这是正确使用条件变量的关键。

C ++标准说,当使用谓词调用时,condition_variable :: wait()的行为类似于以下内容(如本例所示):

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

当消费者检查i

时,可能会出现两种情况
  • 如果i为0,则消费者调用cv.wait(),那么当调用实现的i部分时,wait(lock)仍将为0使用锁确保。在这种情况下,生产者没有机会在其condition_variable::notify_one()循环中调用while,直到消费者调用cv.wait(lk, []{return i == 1;})之后(wait()调用已完成所需的一切要正确地抓住通知 - wait()不会释放锁定,直到完成锁定为止。因此,在这种情况下,消费者不能错过通知。

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

此处的示例确实具有额外的复杂性,即使用done变量向消费者已识别i == 1的生产者线程发出信号,但我不认为这会改变分析完全是因为done(对于阅读和修改)的所有访问都是在涉及icondition_variable的相同关键部分中完成的。

如果您查看@ eh9指向的问题Sync is unreliable using std::atomic and 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()时执行了#{1}}#3}。但是在步骤#1中检查f->resume_mutex是否有必要在完全保持锁定的情况下完成(更不用说连续检查等待),这是正确使用条件变量的要求)。我相信那个代码段有问题的人认为,因为wait()f->counter类型,这符合要求。但是,std::atomic提供的原子性不会延伸到后续对std::atomic的调用。在此示例中,在检查f->resume.wait(lock)(步骤#1)和调用f->counter(步骤#3)之间存在竞争。

在这个问题的例子中,这个种族不存在。

答案 1 :(得分:8)

情况

使用vc10和Boost 1.56我实现了一个非常类似this blog post建议的并发队列。作者解锁互斥锁以最小化争用,即在解锁互斥锁的情况下调用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 documentation

中的示例提供支持
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()

这意味着至少在我的特定线程实现中,在调用monochromeFilter = [CIFilter filterWithName:@"CIColorMonochrome" keysAndValues: @"inputColor", [CIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0f], @"inputIntensity", [NSNumber numberWithFloat:1.5f], nil]; 之前不能解锁互斥锁,尽管两种方式看起来都是正确的。

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:1)

在某些情况下,当cv可能被其他线程占用(锁定)时。在通知_ *()之前,你需要锁定并释放它 如果没有,则可能根本不执行notify _ *()。

答案 4 :(得分:1)

正如其他人所指出的,就竞争条件和与线程相关的问题而言,调用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)行结尾的作用域)。这样,我们确保线程tnotify_one之前调用cv.wait可以检查新设置的谓词变量并继续执行,因为它需要获取锁,t目前持有,要进行检查。因此,我们确保在cv返回之后,线程t不会访问foo

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

答案 5 :(得分:0)

只需添加此答案,因为我认为已接受的答案可能会产生误导。在所有情况下,您都需要在调用notify_one() somewhere 之前锁定互斥体,以使代码具有线程安全性,尽管您可能会在实际调用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()的调用!该程序将无限期地死锁。

由于这个原因,我们应该无法调用notify_one()直到,我们才调用了wait()。请注意,条件变量的功能在于可以自动解锁互斥体,检查是否发生了对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();
  }
}

[... same 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()之前调用它。 / p>

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

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

更常见的情况是,一个线程只是在等待条件变为真,而另一个线程在更改该条件所涉及的变量之前将其锁住(这可能使其变为真)。在这种情况下,互斥锁 在条件变为真之前(和之后)立即被锁定-因此,在这种情况下,只需在调用notify _ *()之前解锁互斥锁就可以了。

相关问题