Pet*_*mit 72 c++ multithreading condition-variable
我对使用有点困惑std::condition_variable.我明白我必须创建unique_lock一个mutex调用之前condition_variable.wait().我无法找到是我是否也应该调用之前获得一个独特的锁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();
}
Run Code Online (Sandbox Code Playgroud)
这里没有为第一个获取锁定notify_one(),而是在第二个时间获取锁定notify_one().通过示例查看其他页面我看到了不同的东西,大多数都没有获取锁定.
notify_one()锁吗?为什么我会选择锁定它?notify_one(),但是后续调用.这个例子是错的还是有一些理由?Mic*_*urr 65
你不需要在调用时持一个锁condition_variable::notify_one(),但从某种意义上来说,它仍然是定义良好的行为而不是错误.
但是,它可能是一个"悲观化",因为任何等待线程都可以运行(如果有的话)将立即尝试获取通知线程持有的锁.我认为在调用notify_one()或调用时避免保持与条件变量关联的锁是一个很好的经验法则notify_all().请参阅Pthread Mutex:pthread_mutex_unlock()会占用大量时间,例如在调用pthread之前释放锁,相当于notify_one()提高性能.
请记住,循环中的lock()调用在while某些时候是必要的,因为在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);
Run Code Online (Sandbox Code Playgroud)
消费者检查时可能会出现两种情况i:
if i为0然后消费者调用cv.wait(),然后在调用实现i的wait(lock)部分时仍为0 - 正确使用锁确保了.在这种情况下,生产者没有机会在消费者调用之前调用condition_variable::notify_one()它的while循环cv.wait(lk, []{return i == 1;})(并且wait()调用已经完成了正确'捕获'通知所需的一切 - wait()在它完成之前不会释放锁定).所以在这种情况下,消费者不能错过通知.
如果i消费者调用时已经为1 cv.wait(),wait(lock)则永远不会调用实现的部分,因为while (!pred())测试将导致内部循环终止.在这种情况下,调用notify_one()的时间并不重要 - 消费者不会阻止.
这里的示例确实具有额外的复杂性,即使用done变量向消费者已经认识到的生产者线程发出信号i == 1,但我认为这根本不会改变分析,因为所有访问权限done(用于读取和修改) )在涉及i和的相同关键部分完成condition_variable.
如果你看一下@ eh9指出的问题,使用std :: atomic和std :: condition_variable,Sync是不可靠的,你会看到一个竞争条件.但是,在该问题中发布的代码违反了使用条件变量的基本规则之一:在执行检查和等待时,它不包含单个关键部分.
在该示例中,代码如下所示:
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)
}
Run Code Online (Sandbox Code Playgroud)
您会注意到wait()#3是在执行时执行的f->resume_mutex.但对于是否该检查wait()是必要的步骤#1 不同时保持在所有的锁(更不用说连续退房和等待),这是正确使用的条件变量)的要求进行.我相信那个代码片段有问题的人认为,因为这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)
}
Run Code Online (Sandbox Code Playgroud)
假设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调用可以检查新设置的谓词变量并继续,因为它需要获取 当前持有的锁来进行检查。所以,我们保证返回后不被线程访问。notify_onecv.waittcvtfoo
总而言之,这种特定情况下的问题实际上并不与线程有关,而是与通过引用捕获的变量的生命周期有关。cv通过 thread 通过引用捕获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
}
Run Code Online (Sandbox Code Playgroud)
解锁互斥锁由Boost文档中的示例支持:
void prepare_data_for_processing()
{
retrieve_data();
prepare_data();
{
boost::lock_guard<boost::mutex> lock(mut);
data_ready=true;
}
cond.notify_one();
}
Run Code Online (Sandbox Code Playgroud)
这仍导致以下不稳定的行为:
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
}
Run Code Online (Sandbox Code Playgroud)
这意味着至少在我的特定线程实现中,在调用之前不能解锁互斥锁boost::condition_variable::notify_one(),尽管两种方式看起来都是正确的.
| 归档时间: |
|
| 查看次数: |
23096 次 |
| 最近记录: |