notify_one 只有在有等待线程时才正确?

notify_one only when there are waiting threads is correct?

我已经多次看到以下类型的调度队列实现,其中将新元素推送到队列的线程仅在推送元素之前队列为空时调用 notify_one 。这种情况将减少不必要的 notify_one 调用,因为 q.size() != 0 在推送新元素之前意味着只有活动线程(假设有多个消费者线程)。

#include <queue>
#include <condition_variable>
#include <mutex>

using Item = int;
std::queue<Item> q;
std::condition_variable cv;
std::mutex m;

void process(Item i){}

void pop() {
  while (true) {
    std::unique_lock<std::mutex> lock(m);
    // This thread releases the lock and start waiting.
    // When notified, the thread start trying to re-acquire the lock and exit wait().
    // During the attempt (and thus before `pop()`), can another `push()` call acquire the lock? 
    cv.wait(lock, [&]{return !q.empty();});
    auto item = q.front();
    q.pop();
    process(item);
  }
}

void push(Item i) {
  std::lock_guard<std::mutex> lock(m);
  q.push(i);
  if (q.size() == 1) cv.notify_one();
}

int main() { /* ... */ }

但是,是否可能出现以下情况?假设所有的消费者线程都在等待。

  1. 推送线程获取锁并推送一个新元素并调用 notify_one 因为队列为空。
  2. 一个被通知的线程试图重新获取锁(并退出 wait())
  3. 推送线程再次获取锁并推送另一个元素在通知线程重新获取锁之前

在这种情况下,在 3 之后不会发生任何 notify_one 调用。并且当队列不为空时,只有一个活动线程。

唤醒后的操作

根据wake's doc

  1. Atomically unlocks lock, blocks the current executing thread, and adds it to the list of threads waiting on *this. The thread will be unblocked when notify_all() or notify_one() is executed. It may also be unblocked spuriously. When unblocked, regardless of the reason, lock is reacquired and wait exits.

意味着你的代码在 #1#2 之间是原子的,当它被唤醒时。无需担心同步问题,因为您的编译器应该会处理它。

void pop() {
  while (true) {
    std::unique_lock<std::mutex> lock(m);

    // When sleep, cv release the lock
    cv.wait(lock, [&]{return !q.empty();});   // <----#1
    // When wake up, it acquires the lock
    auto item = q.front();
    q.pop();
    process(item);                            // <----#2
  }
}

对于虚假唤醒

带谓词的第二个重载将为您处理虚假唤醒。 template< class Predicate > void wait( std::unique_lock<std::mutex>& lock, Predicate stop_waiting ); (2) (since C++11)

  1. Equivalent to
while (!stop_waiting()) {
    wait(lock);
}

This overload may be used to ignore spurious awakenings while waiting for a specific condition to become true.

Note that lock must be acquired before entering this method, and it is reacquired after wait(lock) exits, which means that lock can be used to guard access to stop_waiting().

那就是 condition_variable 谓词重载可以避免虚假唤醒。 不需要用于检查虚假唤醒的手工 while 循环。