仅添加此答案是因为我认为已接受的答案可能会误导您。在所有情况下,您都需要先锁定互斥锁,然后才能在某个地方调用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)
cv.notify_one();
}
bool start()
{
if (count.fetch_add(1) >= 0)
return true;
stop();
return false;
}
void cancel()
{
if (count.fetch_sub(1000) == 0)
return;
std::unique_lock<std::mutex> lk(cancel_mutex);
cancel_cv.wait(lk);
}
警告:此代码包含错误。
这个想法是这样的:线程成对调用start()和stop(),但前提是start()返回true。例如:
if (start())
{
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()的调用!该程序将无限期陷入僵局。
因此,在调用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)
{
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 _ *()之前解锁互斥锁是完全可以的。
wait morphing
优化)此链接中解释的经验法则:在具有2个以上线程的情况下,使用WITH锁通知更好,以获得更可预测的结果。