我必须在调用condition_variable.notify_one()之前获得锁吗?< / h1 >

Do I have to acquire lock before calling condition_variable.notify_one()?

本文关键字:gt h1 lt 调用 condition variable one notify      更新时间:2023-10-16

我对std::condition_variable的使用有点困惑。我知道我必须在调用condition_variable.wait()之前在mutex上创建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 == 1n";
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()没有锁,但随后的调用有锁。这个例子是错的还是有道理的?

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

然而,这可能是一种"悲观"。因为无论什么等待线程被设置为可运行的(如果有的话),都会立即尝试获取发出通知的线程持有的锁。我认为在调用notify_one()notify_all()时避免持有与条件变量相关的锁是一个很好的经验法则。pthread_mutex_unlock()消耗了大量的时间,在调用等价于notify_one()的Pthread之前释放一个锁可以显著提高性能。

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


2016-02-27:大更新,以解决评论中的一些问题,如果锁没有为notify_one()调用持有,是否存在竞争条件。我知道这个更新很晚,因为这个问题几乎是两年前问的,但是我想解决@Cookie的问题,如果生产者(在这个例子中是signals())在消费者(在这个例子中是waits())能够调用wait()之前调用notify_one(),那么可能存在竞争条件。

关键是i发生了什么——这个对象实际上指示消费者是否有"工作"。要做的事情。condition_variable只是一个机制,让消费者有效地等待i的变化。

生产者在更新i时需要持有锁,消费者在检查i和调用condition_variable::wait()时必须持有锁(如果需要等待的话)。在这种情况下,关键是,当使用者执行此检查和等待时,它必须与持有锁(通常称为临界区)的实例相同。因为当生产者更新i和消费者检查并等待i时,临界区被保持,所以当消费者检查i和调用condition_variable::wait()时,i没有机会改变。这是正确使用条件变量的关键。

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()在完成之前不会释放锁)。因此,在这种情况下,消费者不会错过通知。

  • 如果i在消费者调用cv.wait()时已经为1,则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)
}

您将注意到#3的wait()是在按住f->resume_mutex时执行的。但是,在步骤#1中检查wait()是否必要时,根本没有在持有该锁时执行(更不用说连续检查和等待了),这是正确使用条件变量的要求)。我相信对该代码片段有问题的人认为,由于f->counterstd::atomic类型,这将满足要求。但是,std::atomic提供的原子性并没有扩展到对f->resume.wait(lock)的后续调用。在这个例子中,在检查f->counter的时间(步骤#1)和调用wait()的时间(步骤#3)之间存在竞争。

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

正如其他人指出的那样,在调用notify_one()时,就竞争条件和线程相关问题而言,您不需要持有锁。然而,在某些情况下,可能需要持有锁来防止condition_variablenotify_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)行结束的作用域)。通过这样做,我们可以确保线程tcv.wait检查新设置的谓词变量并继续之前调用notify_one,因为它需要获取t当前持有的锁来进行检查。因此,我们确保在foo返回后,线程t不会访问cv

总而言之,这个特定情况下的问题实际上与线程无关,而是与引用捕获的变量的生命周期有关。cv是通过线程t通过引用捕获的,因此您必须确保cv在线程执行期间保持活动状态。这里提供的其他示例没有这个问题,因为condition_variablemutex对象是在全局作用域中定义的,因此它们保证在程序退出之前保持活动状态。

情况

使用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()之前不能被解锁,尽管两种方法看起来都是正确的。

我只是加上这个答案,因为我认为公认的答案可能会误导。在所有情况下,您都需要在调用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()返回后将销毁在"Do stuff"中需要的对象。然而,当start()和stop()之间有线程时,cancel()应该不会返回,并且一旦cancel()执行了第一行,start()将始终返回false,因此没有新线程将进入'Do stuff'区域。

工作对吗?

推理如下:

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

2)同样,当一个线程成功执行了start()的第一行,但还没有执行stop()的第一行时,那么任何线程都不可能成功执行cancel()的第一行(注意,只有一个线程调用了cancel()): fetch_sub(1000)返回的值将大于0。

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

4)调用start()和stop()的次数总是平衡的,所以在cancel()的第一行执行失败后,总是会有一个时刻(最后一次)调用stop()导致count达到-1000,因此调用notify_one()。注意,只有当第一行cancel导致线程失败时才会发生这种情况。

除了一个饥饿问题(许多线程调用start()/stop(),计数永远不会达到-1000,cancel()永远不会返回,这可能被认为是"不太可能且永远不会持续很长时间")之外,还有另一个bug:

这是可能的,有一个线程在'Do stuff'区域,让我们说它只是调用stop();此时,线程执行cancel()的第一行,使用fetch_sub(1000)读取值1,然后退出。但是在它获取互斥锁和/或调用wait(lk)之前,第一个线程执行stop()的第一行,读取-999并调用cv.notify_one()!

那么对notify_one()的调用在我们等待()条件变量之前完成!程序会无限死锁。

因此我们不能在之前调用notify_one()我们调用了wait()。请注意,条件变量的强大之处在于它能够自动解锁互斥锁,检查是否发生了对notify_one()的调用,以及是否进入睡眠状态。你骗不了它,但是可以当对变量进行更改时,可能会使条件从false更改为true,因此需要保持互斥锁,并且保持当调用notify_one()时,由于这里描述的竞争条件,它被锁定。

在这个例子中没有条件。为什么我没有使用条件'count == -1000'?因为这一点都不有趣:只要达到-1000,我们确信没有新的线程将进入"做事情"区域。此外,线程仍然可以调用start()并增加计数(到-999和-998等),但我们不关心这个。唯一重要的是达到了-1000,这样我们就可以确定在"Do stuff"区域中再也没有线程了。我们确信这是在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_*()之前解锁互斥锁是完全可以的。

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

在给定的示例中,锁是由并发使用变量i激发的。因为signals线程修改变量,所以它需要确保在此期间没有其他线程访问它。

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

在某些情况下,当cv可能被其他线程占用(锁定)时。您需要在notify_*()之前获得锁并释放它。
如果不是,notify_*()可能根本不执行。

据我所知,notify_one调用pthread_cond_signal。如果是这样,你对此有何看法?

对于可预测的调度行为和防止丢失的唤醒,在给条件变量发信号时应该保持互斥锁。

https://www.unix.com/man-page/hpux/3T/pthread_cond_signal/

所有等待条件变量的线程都被挂起,直到另一个线程使用信号函数:

pthread_cond_signal(及myConVar);

在这种情况下,互斥锁必须在调用函数之前被锁定,然后在调用函数之后被解锁。

https://www.i-programmer.info/programming/cc/12288-fundamental-c-condition-variables.html

我个人遇到过通知丢失的情况,因为调用notify_one时没有锁定互斥锁。