在 condition_variable::notify_all() 之后或之前解锁互斥量?

unlock the mutex after condition_variable::notify_all() or before?

查看 several videos and the documentation example,我们在 调用 notify_all() 之前解锁互斥量 。在之后调用它会更好吗?

常用方式:

通知线程内部:

//prepare data for several worker-threads;

//and now, awaken the threads:
std::unique_lock<std::mutex> lock2(sharedMutex);
_threadsCanAwaken = true;

lock2.unlock(); 
_conditionVar.notify_all(); //awaken all the worker threads;

//wait until all threads completed;

//cleanup:
_threadsCanAwaken = false;

//prepare new batches once again, etc, etc

在其中一个工作线程中:

while(true){
    // wait for the next batch:

    std::unique_lock<std::mutex> lock1(sharedMutex);
    _conditionVar.wait(lock1,  [](){return _threadsCanAwaken});
    lock1.unlock(); //let sibling worker-threads work on their part as well

    //perform the final task

    //signal the notifier that one more thread has completed;

    //loop back and wait until the next task
}

注意 lock2 在我们通知条件变量之前是如何解锁的——我们是否应该在 notify_all() 之后解锁它?

编辑

来自我下面的评论:我担心的是,如果消费者超快怎么办。消费者虚假地醒来,看到互斥锁被解锁,完成任务并循环回到 while 的开始。现在,慢戳的 Producer 终于调用了 notify_all(),导致 consumer 多循环了一次。

should we instead unlock it after the notify_all() ?

这两种方式都是正确的,但在不同的情况下你可能会有不同的行为。很难预测它将如何影响程序的性能——我已经看到了对不同应用程序的正面和负面影响。因此,最好分析您的程序并根据分析对您的特定情况做出决定。

如此处所述:cppreference.com

The notifying thread does not need to hold the lock on the same mutex as the one held by the waiting thread(s); in fact doing so is a pessimization, since the notified thread would immediately block again, waiting for the notifying thread to release the lock.

也就是说,documentation for wait

At the moment of blocking the thread, the function automatically calls lck.unlock(), allowing other locked threads to continue.

Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock(), leaving lck in the same state as when the function was called. Then the function returns (notice that this last mutex locking may block again the thread before returning).

因此,当通知等待将重新尝试获取锁,并且在该过程中它将再次被阻塞,直到原始通知线程释放锁。 所以我会建议在调用通知之前释放锁。正如 cppreference.com 中的示例所做的,最重要的是

Don't be Pessimistic.

My concern is that, what if consumer is super quick. Consumer spuriously awakes, sees that the mutex is unlocked, completes the task and loops back to the start of while. Now the slow-poke Producer finally calls notify_all(), causing consumer to loop an additional time.

来自 cpp reference:

When the condition variable is notified, a timeout expires, or a spurious wakeup occurs, the thread is awakened, and the mutex is atomically reacquired. The thread should then check the condition and resume waiting if the wake up was spurious.

// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
cv.notify_one();

除非您的实现不寻常,否则在向条件变量发出信号之前解锁互斥锁没有任何优势。发信号前解锁有两个缺点:

  1. 如果您在发出信号之前解锁,信号可能会唤醒一个线程,该线程会在您解锁后选择在条件变量上阻塞。如果您使用相同的条件变量来指示多个逻辑条件,这可能会导致死锁。这种错误很难创建、难以诊断和理解。通过在解锁前始终发出信号可以轻松避免这种情况。这确保共享状态和信号的更改是一个原子操作,并且竞争条件和死锁是不可能的。

  2. 发信号前解锁会降低性能,而发信号后解锁可以避免这种情况。如果你在解锁之前发出信号,一个好的实现会知道你的信号不可能使任何线程准备好-运行,因为互斥量由调用线程持有,并且任何受条件变量影响的线程必然不能向前没有互斥量的进展。这允许进行显着优化(通常称为“等待变形”),如果您先解锁则不可能。

所以在持有锁的同时发出信号,除非你有一些不寻常的理由不这样做。