只是添加这个答案,因为我认为接受的答案可能会产生误导。在所有情况下,您都需要在调用 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_*() 之前解锁互斥锁是完全可以的。