[英]Do I need to lock the mutex before calling condition_variable::notify()?
[英]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 == 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()
没有锁,但后续调用有。 这个例子是错误的还是有一些理由?调用condition_variable::notify_one()
时不需要持有锁,但从某种意义上说它仍然是明确定义的行为而不是错误,这并没有错。
但是,这可能是一种“悲观化”,因为任何等待线程变为可运行(如果有)都会立即尝试获取通知线程持有的锁。 我认为在调用notify_one()
或notify_all()
时避免持有与条件变量关联的锁是一个很好的经验法则。 请参阅Pthread Mutex:pthread_mutex_unlock() 消耗大量时间的示例,其中在调用与notify_one()
等效的 pthread 之前释放锁可显着提高性能。
请记住, while
循环中的lock()
调用在某些时候是必要的,因为在while (!done)
循环条件检查期间需要保持锁。 但是对于notify_one()
的调用不需要保留它。
2016-02-27 :大型更新解决了评论中的一些问题,即如果没有为notify_one()
调用保留锁,是否存在竞争条件。 我知道这个更新迟了,因为这个问题是在大约两年前被问到的,但我想解决@Cookie 的问题,即如果生产者(本例中的signals()
在消费者之前调用notify_one()
,可能会出现竞争条件(本例中为waits()
)能够调用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()
,那么当调用实现的wait(lock)
部分时i
仍将为 0 - 正确使用锁可确保这一点。 在这种情况下,生产者没有机会在其while
循环中调用condition_variable::notify_one()
直到消费者调用cv.wait(lk, []{return i == 1;})
(和wait()
call 已经完成了正确“捕获”通知所需的一切 - wait()
在完成之前不会释放锁)。 所以在这种情况下,消费者不能错过通知。
如果在消费者调用cv.wait()
时i
已经为 1,则永远不会调用实现的wait(lock)
部分,因为while (!pred())
测试将导致内部循环终止。 在这种情况下,调用 notify_one() 的时间并不重要——消费者不会阻塞。
这里的示例确实具有额外的复杂性,即使用done
变量向生产者线程发信号通知消费者已经认识到i == 1
,但我认为这根本不会改变分析,因为所有对done
的访问(用于阅读和修改)在涉及i
和condition_variable
的相同关键部分中完成。
如果您查看@eh9 指出的问题, Sync is unreliable using 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。 线程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东西的区域。
工作正常吗?
推理如下:
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()。 请注意,只有在第一行取消导致该线程失败时才会发生这种情况。
除了这么多线程正在调用 start()/stop() 计数永远不会达到 -1000 并且 cancel() 永远不会返回的饥饿问题(人们可能会接受它为“不太可能且永远不会持续很长时间”)之外,还有另一个错误:
'Do stuff' 区域内可能有一个线程,可以说它只是调用 stop(); 在那一刻,一个线程执行 cancel() 的第一行,使用 fetch_sub(1000) 读取值 1 并失败。 但在它使用互斥锁和/或调用wait(lk)之前,第一个线程执行stop()的第一行,读取-999并调用cv.notify_one()!
然后在我们等待条件变量之前完成对 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_*() 之前解锁互斥锁是完全可以的。
@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。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.