我必须在调用condition_variable.notify_one()之前获得锁吗?< / h1 >
Do I have to acquire lock before calling condition_variable.notify_one()?
我对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
的访问(包括读取和修改)都是在涉及i
和condition_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->counter
是std::atomic
类型,这将满足要求。但是,std::atomic
提供的原子性并没有扩展到对f->resume.wait(lock)
的后续调用。在这个例子中,在检查f->counter
的时间(步骤#1)和调用wait()
的时间(步骤#3)之间存在竞争。
这个问题的例子中不存在这个种族。
正如其他人指出的那样,在调用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
在cv.wait
检查新设置的谓词变量并继续之前调用notify_one
,因为它需要获取t
当前持有的锁来进行检查。因此,我们确保在foo
返回后,线程t
不会访问cv
。
总而言之,这个特定情况下的问题实际上与线程无关,而是与引用捕获的变量的生命周期有关。cv
是通过线程t
通过引用捕获的,因此您必须确保cv
在线程执行期间保持活动状态。这里提供的其他示例没有这个问题,因为condition_variable
和mutex
对象是在全局作用域中定义的,因此它们保证在程序退出之前保持活动状态。
情况
使用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时没有锁定互斥锁。
- EASTL矢量<向量<int>>连续的
- C - 创建矢量&lt; vector&lt; double&gt;&gt;矩阵具有分配而不是inizializ
- C 字符串比较“祝您好运”&gt;“再见”
- 为什么将此对向量&lt; map&lt; int,int&gt;&gt;中的地图进行更新.失败
- C :对矢量进行排序&lt; struct&gt;(结构有2个整数)基于结构的整数之一
- C 操作员&gt;&gt;与突变器过载
- 明确的专业化“ CheckIntmap&lt;&gt;”实例化
- 是否需要使用 - &gt;运算符在C 中调用成员函数时
- 什么是模板&lt;&gt;inline bla bla
- 编辑C Qlist&lt; object*&gt; gt;QML代码和一些QML警告中的模型
- eigen :: llt&lt;eigen :: matrixxd&gt;具有不完整的类型
- 错误,包括&lt; ctype&gt;在原子上使用C 11
- 错误c++visual studio c2227左侧'->;Init';必须指向类/结构/联合/泛型类型
- std::vector<;uint8_t>;当C++11/14启用时,手动复制而不是调用memcpy
- ``这个''不能用this-&gt;指针变量
- 如何加入向量&lt; int&gt;到C 中的单个INT
- 是std :: set&lt; std :: future&gt;不可能存在
- 使用shared_ptr<字符串>转换为一个无序集合<字符串>
- 是numeric_limits&lt; int&gt; :: is_modulo从逻辑上矛盾
- opencv 2.4.7在iOS错误背景_segm.hpp #include&lt; list&gt;未找到