通知条件变量后使用互斥锁

Use of mutex after condition variable has been notified

本文关键字:条件 变量 通知      更新时间:2023-10-16

通知的条件变量在收到通知后重新锁定互斥锁的原因是什么?

以下代码段死锁(如果unique_lock没有作用域或互斥锁未显式解锁(

#include <future>
#include <mutex>
#include <iostream>
using namespace std;
int main()
{
std::mutex mtx;
std::condition_variable cv;
//simulate another working thread sending notification
auto as = std::async([&cv](){   std::this_thread::sleep_for(std::chrono::seconds(2));
cv.notify_all();});
//uncomment scoping (or unlock below) to prevent deadlock 
//{
std::unique_lock<std::mutex> lk(mtx);
//Spurious Wake-Up Prevention not adressed in this short sample
//UNLESS it is part of the answer / reason to lock again
cv.wait(lk);
//}
std::cout << "CV notifiedn" << std::flush;
//uncomment unlock (or scoping  above) to prevent deadlock 
//mtx.unlock();
mtx.lock();
//do something
mtx.unlock();
std::cout << "End may never be reachedn" << std::flush;
return 0;
}

即使重新阅读一些文档和示例,我仍然没有发现这一点很明显。

可以通过网络找到的大多数示例都是具有固有unique_lock范围的小型代码示例。

我们是否应该使用不同的互斥锁来处理关键部分(互斥锁 1(和条件变量等待并通知(互斥锁 2(?

注意:调试显示,在等待阶段结束后,"内部"互斥计数"(我认为结构__pthread_mutex_s的字段__count(从 1 变为 2。解锁后返回 0

您正在尝试锁定互斥锁两次。 一次使用unique_lock,再次使用显式mutex.lock()调用。对于非递归互斥锁,它将在重新锁定尝试时死锁,让您知道您有错误。

std::unique_lock<std::mutex> lk(mtx);   // This locks for the lifetime of the unique_lock object
cv.wait(lk);  // this will unlock while waiting, but relock on return
std::cout << "CV notifiedn" << std::flush;
mtx.lock();  // This attempts to lock the mutex again, but will deadlock since unique_lock has already invoked mutex.lock() in its constructor.

该修复程序非常接近您未注释的那些大括号。只需确保在互斥锁上一次只有一个锁处于活动状态即可。

此外,正如您拥有的那样,您的代码很容易出现虚假唤醒。 以下是一些调整。 您应该始终保持在等待循环中,直到条件或状态(通常由互斥锁本身保护(实际发生。 对于简单的通知,布尔值就可以了。

int main()
{
std::mutex mtx;
std::condition_variable cv;
bool conditon = false;
//simulate another working thread sending notification
auto as = std::async([&cv, &mtx, &condition](){   
std::this_thread::sleep_for(std::chrono::seconds(2));
mtx.lock();
condition = true;
mtx.unlock();
cv.notify_all();});
std::unique_lock<std::mutex> lk(mtx); // acquire the mutex lock
while (!condition)
{
cv.wait(lk);
}
std::cout << "CV notifiedn" << std::flush;
//do something - while still under the lock
return 0;
}

因为条件等待可能出于通知(如信号(之外的原因返回,或者只是因为其他人写入了相同的 64 字节缓存行。或者它可能已被通知,但条件不再为 true,因为另一个线程处理了它。

因此,互斥锁是锁定的,以便您的代码可以在持有互斥锁的同时检查其条件变量。也许这只是一个布尔值,表示它已准备就绪。

不要跳过该部分。如果你这样做,你会后悔的。

让我们暂时想象mutexwait返回时没有被锁定:

线程 1:

锁定mutex,检查谓词(无论可能是什么(,并在发现谓词不是可接受的形式时,等待其他线程将其置于可接受的形式。 等待以原子方式使线程 1 进入睡眠状态并解锁mutex。 解锁mutex后,其他线程将有权将谓词置于可接受的状态(谓词本身不是线程安全的(。

线程 2:

同时,此线程正在尝试锁定mutex并将谓词置于线程 1 可接受的状态,以继续等待。 它必须在锁定mutex的情况下执行此操作。mutex保护谓词不被多个线程一次访问(读取或写入(。

一旦线程 2 将mutex置于可接受的状态,它就会通知condition_variable并解锁mutex(这两个操作的顺序与此参数无关(。

线程 1:

现在线程 1 已被通知,我们假设假设mutex在从wait返回时未被锁定。 线程 1 要做的第一件事是检查谓词以查看它是否真的可以接受(这可能是一个虚假的唤醒(。 但它不应该在没有锁定mutex的情况下检查谓词。 否则,其他线程可能会在此线程检查谓词后立即更改谓词,从而使该检查的结果无效。

所以这个线程在唤醒时要做的第一件事就是锁定mutex然后检查谓词。

因此,从wait返回时锁定mutex实际上更方便。 否则,等待线程将不得不在 100% 的时间内手动锁定它。


让我们再看一下线程 1 进入wait的事件:我说过睡眠和解锁是原子发生的。 这一点非常重要。 想象一下,如果线程 1 必须手动解锁mutex然后调用wait:在这个假设的场景中,线程 1 可以解锁mutex,然后在另一个线程获取mutex、更改谓词、解锁mutex并发出condition_variable信号时被中断,所有这些都在线程 1 调用wait之前。 现在线程 1 永远休眠,因为没有线程会看到谓词需要更改,而condition_variable需要信号。

因此,unlock/enter-wait必须以原子方式发生。 如果lock/exit-wait也是原子发生的,它会使 API 更易于使用。