我对使用std::condition_variable
感到有点困惑。我知道在调用unique_lock
之前,我必须在mutex
上创建condition_variable.wait()
。我无法找到的是在致电notify_one()
或notify_all()
之前是否还应该获得一个独特的锁。
cppreference.com上的示例存在冲突。例如,notify_one page给出了这个例子:
#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()
没有锁定,但后续调用有效。这个例子是错的还是有一些理由?答案 0 :(得分:56)
在致电condition_variable::notify_one()
时,您无需持锁,但从某种意义上说,它仍然是明确定义的行为,而不是错误。
但是,它可能是一个悲观的&#34;因为任何等待线程都是可运行的(如果有的话)将立即尝试获取通知线程所持有的锁。我认为在调用notify_one()
或notify_all()
时避免持有与条件变量关联的锁是一个很好的经验法则。有关释放锁定的示例,请参阅Pthread Mutex: pthread_mutex_unlock() consumes lots of time,然后在调用相当于notify_one()
的pthread时,可以显着提高性能。
请记住,lock()
循环中的while
调用在某些时候是必要的,因为在while (!done)
循环条件检查期间需要保持锁定。但是,对notify_one()
的调用不需要保留。
2016-02-27 :大量更新,以解决有关竞争状况是否锁定的评论中的一些问题对notify_one()
的帮助不起作用呼叫。我知道这个更新已经晚了,因为这个问题差不多是在两年前提出来的,但是如果生产者(本例中为signals()
),我想解决@Cookie关于可能的竞争条件的问题。在消费者(本例中为notify_one()
)之前调用waits()
就可以调用wait()
。
关键是i
发生的事情 - 实际上表明消费者是否有工作的对象&#34;去做。 condition_variable
只是让消费者有效等待更改为i
的机制。
生产者需要在更新i
时保持锁定,并且消费者必须在检查i
并调用condition_variable::wait()
时保持锁定(如果需要等待)。在这种情况下,关键是当消费者执行此检查和等待时,它必须是持有锁(通常称为临界区)的相同实例。由于关键部分是在生产者更新i
时以及当消费者在i
上检查并等待时保留的,因此i
没有机会在消费者检查i
之间进行更改1}}以及何时调用condition_variable::wait()
。这是正确使用条件变量的关键。
C ++标准说,当使用谓词调用时,condition_variable :: wait()的行为类似于以下内容(如本例所示):
while (!pred())
wait(lock);
当消费者检查i
:
如果i
为0,则消费者调用cv.wait()
,那么当调用实现的i
部分时,wait(lock)
仍将为0使用锁确保。在这种情况下,生产者没有机会在其condition_variable::notify_one()
循环中调用while
,直到消费者调用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指向的问题Sync is unreliable using std::atomic and 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)
}
您会注意到在wait()
时执行了#{1}}#3}。但是在步骤#1中检查f->resume_mutex
是否有必要在完全保持锁定的情况下不完成(更不用说连续检查等待),这是正确使用条件变量的要求)。我相信那个代码段有问题的人认为,因为wait()
是f->counter
类型,这符合要求。但是,std::atomic
提供的原子性不会延伸到后续对std::atomic
的调用。在此示例中,在检查f->resume.wait(lock)
(步骤#1)和调用f->counter
(步骤#3)之间存在竞争。
在这个问题的例子中,这个种族不存在。
答案 1 :(得分:8)
使用vc10和Boost 1.56我实现了一个非常类似this blog post建议的并发队列。作者解锁互斥锁以最小化争用,即在解锁互斥锁的情况下调用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 documentation:
中的示例提供支持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()
这意味着至少在我的特定线程实现中,在调用monochromeFilter = [CIFilter filterWithName:@"CIColorMonochrome" keysAndValues: @"inputColor", [CIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0f], @"inputIntensity", [NSNumber numberWithFloat:1.5f], nil];
之前不能解锁互斥锁,尽管两种方式看起来都是正确的。
答案 2 :(得分:1)
condition_variable::notify_one
不需要锁定变量。但是,在这种情况下,没有什么能阻止你使用锁定,正如示例所示。
在给定的示例中,锁的动机是同时使用变量i
。由于signals
线程修改了变量,因此需要确保在此期间没有其他线程访问它。
锁用于任何需要同步的情况,我认为我们不能以更一般的方式说明它。
答案 3 :(得分:1)
在某些情况下,当cv可能被其他线程占用(锁定)时。在通知_ *()之前,你需要锁定并释放它 如果没有,则可能根本不执行notify _ *()。
答案 4 :(得分:1)
正如其他人所指出的,就竞争条件和与线程相关的问题而言,调用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
在notify_one
之前调用cv.wait
可以检查新设置的谓词变量并继续执行,因为它需要获取锁,t
目前持有,要进行检查。因此,我们确保在cv
返回之后,线程t
不会访问foo
。
总而言之,在这种特定情况下,问题实际上与线程无关,而是与由引用捕获的变量的生存期有关。 cv
是通过线程t
的引用捕获的,因此您必须确保cv
在线程执行期间保持活动状态。这里提供的其他示例不会遇到此问题,因为condition_variable
和mutex
对象是在全局范围内定义的,因此可以保证它们保持活动状态,直到程序退出。
答案 5 :(得分:0)
只需添加此答案,因为我认为已接受的答案可能会产生误导。在所有情况下,您都需要在调用notify_one() somewhere 之前锁定互斥体,以使代码具有线程安全性,尽管您可能会在实际调用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()返回之后,将销毁“执行任务”所需的对象。但是,如果在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()的线程将不再进入“填充”区域。
4)对start()和stop()的调用次数始终是平衡的,因此在cancel()的第一行执行失败后,总会有片刻(最后一次)调用stop() )导致计数达到-1000,因此将调用notify_one()。请注意,只有在取消的第一行导致该线程掉线时,这种情况才会发生。
除了饥饿问题,其中有很多线程正在调用start()/ stop(),计数永远不会达到-1000,cancel()永远不会返回,这可能会被视为“不太可能且永远不会持续很长时间”,还有另一个错误:
“填充”区域中可能有一个线程,可以说它只是调用stop();。此时,线程执行cancel()的第一行,并使用fetch_sub(1000)读取值1并掉线。但是在获取互斥体和/或调用wait(lk)之前,第一个线程执行stop()的第一行,读取-999并调用cv.notify_one()!
然后在对条件变量进行wait()之前,完成对notify_one()的调用!该程序将无限期地死锁。
由于这个原因,我们应该无法调用notify_one()直到,我们才调用了wait()。请注意,条件变量的功能在于可以自动解锁互斥体,检查是否发生了对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();
}
}
[... same 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()之前调用它。 / p>
请注意,在这种情况下,我在调用notify_one()之前已解锁了互斥锁,因为否则,对notify_one()的调用很有可能会唤醒线程,等待条件变量,然后尝试获取条件变量互斥锁和阻止,然后再次释放互斥锁。那只是比需要的慢一点。
此示例有点特殊,因为更改条件的行由调用wait()的同一线程执行。
更常见的情况是,一个线程只是在等待条件变为真,而另一个线程在更改该条件所涉及的变量之前将其锁住(这可能使其变为真)。在这种情况下,互斥锁 在条件变为真之前(和之后)立即被锁定-因此,在这种情况下,只需在调用notify _ *()之前解锁互斥锁就可以了。