仅添加此答案是因为我认为接受的答案可能具有误导性。在所有情况下,您都需要在调用 notify_one() 某处 之前锁定互斥锁,以使您的代码成为线程安全的,尽管您可能会在实际调用 notify_*() 之前再次解锁它。 p>
为了澄清,您必须在进入 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() 的时刻) 导致计数达到 -1000,因此调用 notify_one()。请注意,只有在第一行取消导致该线程失败时才会发生这种情况。
除了一个饥饿问题,其中有这么多线程正在调用 start()/stop() 计数永远不会达到 -1000 并且 cancel() 永远不会返回,人们可能会认为这是“不太可能并且永远不会持续很长时间”,还有另一个错误:
'Do stuff' 区域内可能有一个线程,假设它只是调用 stop();在那一刻,一个线程执行 cancel() 的第一行,使用 fetch_sub(1000) 读取值 1 并失败。但是在它使用互斥体和/或调用wait(lk)之前,第一个线程执行stop()的第一行,读取-999并调用cv.notify_one()!
然后对 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();
}
}
[...相同的开始()...]
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_*() 之前解锁互斥锁是完全可以的。