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

Use of mutex after condition variable has been notified

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

如果 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 notified\n" << std::flush;

    //uncomment unlock (or scoping  above) to prevent deadlock 
    //mtx.unlock();

    mtx.lock();
    //do something
    mtx.unlock();

    std::cout << "End may never be reached\n" << std::flush;

    return 0;
}

即使重新阅读 some documentation and examples 我仍然不觉得这很明显。

可以在网上找到的大多数示例都是小型代码示例,它们具有 unique_lock.

的固有范围

我们是否应该使用不同的互斥锁来处理临界区(互斥锁 1)和条件变量等待和通知(互斥锁 2)?

注意:调试显示等待阶段结束后,"internal" "mutex count"(我认为结构 __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 notified\n" << 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.

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

此外,如您所见,您的代码很容易被虚假唤醒。这里有一些调整给你。您应该始终停留在等待循环中,直到条件或状态(通常由互斥锁本身保护)实际发生。对于简单的通知,bool 就可以了。

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 notified\n" << std::flush;

    //do something - while still under the lock

    return 0;
}

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

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

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

让我们暂时假设 mutex 不是 wait:

锁定在 return

线程 1:

锁定 mutex,检查谓词(无论可能是什么),并在发现谓词不是可接受的形式时,等待其他线程将其放入可接受的形式。等待 原子地 将线程 1 置于睡眠状态并解锁 mutexmutex 解锁后,其他一些线程将有权将谓词置于可接受的状态(谓词自然不是线程安全的)。

线程 2:

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

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

线程 1:

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

所以非常这个线程在醒来时必须做的第一件事是锁定mutex然后检查谓词。

所以mutexwait锁定在return确实更方便。否则,等待线程必须在 100% 的时间内手动锁定它。


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

因此 势在必行 unlock/enter-wait 自动 发生。如果 lock/exit-wait 也自动发生,它会使 API 更容易使用。