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

我对使用std::condition_variable有点困惑。 我知道我必须在调用condition_variable.wait()之前在mutex上创build一个unique_lock 。 我找不到在调用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()之前,我可以select自己locking互斥锁吗?为什么要selectlocking它?
  • 在给出的例子中,为什么第一个notify_one()没有locking,但是对于后续的调用。 这个例子是错的还是有一些基本原理?

调用condition_variable::notify_one() ,不需要保持lockingcondition_variable::notify_one() ,但是它仍然是定义良好的行为,而不是错误。

然而,这可能是一种“悲观化”,因为无论等待线程是否可运行(如果有的话)都会立即尝试获取通知线程持有的锁。 我认为在调用notify_one()notify_all()避免保持与条件variables关联的锁是一个很好的经验法则。 请参阅Pthread Mutex:pthread_mutex_unlock()在调用与notify_one()等效的pthread之前释放锁的示例消耗大量时间 ,可以显着提高性能。

请记住, while循环中的lock()调用在某个时刻是必需的,因为在while (!done)循环条件检查期间需要保持该locking。 但是对于notify_one()的调用并不需要保留。


2016-02-27 :大的更新来解决一些关于是否有竞争条件的注释中的一些问题是锁对notify_one()调用没有帮助。 我知道这个更新是迟到的,因为这个问题在两年前就被问到了,但是如果生产者(这个例子中的signals()在消费者之前调用notify_one() ,我想解决@ Cookie有关可能的竞争条件的问题(在这个例子中waits() )能够调用wait()

关键是我发生了什么 – 这是实际表明消费者是否有“工作”的对象。 condition_variable只是一个让消费者有效等待对i的更改的机制。

生成器在更新i时需要保持locking状态,并且在检查i和调用condition_variable::wait() (如果需要等待的时候)时,消费者必须保持lockingcondition_variable::wait() 。 在这种情况下,关键在于当消费者执行此检查并等待时, 它必须是持有锁的同一实例 (通常称为关键部分)。 由于当生产者更新i时,以及当消费者检查并等待i ,关键部分被保留,所以当消费者检查i以及何时调用condition_variable::wait()时, i没有机会改变。 这是正确使用条件variables的关键。

C ++标准规定condition_variable :: wait()在使用谓词调用时的行为如下所示(在这种情况下):

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

当消费者检查i时有两种情况可能发生:

  • 如果i是0,那么消费者调用cv.wait() ,那么当实现的wait(lock)部分被调用时, i仍然是0 – 正确使用锁确保。 在这种情况下,生产者没有机会在其while循环中调用condition_variable::notify_one() ,直到消费者调用cv.wait(lk, []{return i == 1;}) (和wait()调用已经完成了所有需要做的事情来正确地“捕获”一个通知 – wait()不会释放锁,直到它已经完成)。 所以在这种情况下,消费者不能错过通知。

  • 如果消费者调用cv.wait()i已经是1,那么执行的wait(lock)部分将永远不会被调用,因为while (!pred())testing将导致内部循环终止。 在这种情况下,对notify_one()的调用发生时无关紧要 – 消费者不会阻塞。

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

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

在这个例子中,代码如下所示:

 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) } 

你会注意到#3的wait()是在执行f->resume_mutex 。 但是,在步骤#1中是否需要检查wait()是否完全保持该锁(对于检查和等待而言连续得多less),这是正确使用条件variables的要求) 。 我相信那个有这个代码片段问题的人认为,因为f->counter是一个std::atomictypes,所以这将满足要求。 但是,由std::atomic提供的primefaces性不会扩展到对f->resume.wait(lock)的后续调用。 在这个例子中,当f->counter被检查(步骤#1)和wait()被调用(步骤#3)之间存在竞争。

这个问题的例子中不存在这种竞争。

情况

使用vc10和Boost 1.56我实现了一个并发队列,就像这个博客文章所build议的。 作者解锁互斥量以最小化争用,即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()在互斥锁仍然被locking的情况下被调用,在离开该作用域之后立即被解锁:

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

这意味着至less在我的特定线程实现中,在调用boost::condition_variable::notify_one()之前,互斥量一定不能解锁,尽pipe这两种方法看起来都是正确的。

@迈克尔伯尔是正确的。 condition_variable::notify_one不需要lockingvariables。 尽pipe如此,没有什么能阻止你在这种情况下使用锁。

在给定的例子中,锁的动机是variablesi的并发使用。 因为signals线程修改variables,所以需要确保在此期间没有其他线程访问它。

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