关于std::condition_variable notify_one的线程唤醒行为疑问
问题场景
测试std::condition_variable的边缘场景:设置99个生产者和1个消费者操作有最大容量的队列。队列满后调用notify_one时,命中消费者的概率仅约1%,大概率会唤醒另一个生产者,该生产者检查谓词后会继续等待。
原以为程序会在此处挂起,但实际观察到程序会持续检查谓词直到命中消费者;若没有消费者,则会永久遍历所有等待线程。
核心疑问
- C++标准是否规定
notify_one会尝试唤醒首个谓词结果为真的等待线程? - 为何注释掉谓词的消费者代码也能正常工作?
- 队列满后出现的大量谓词检查是否由虚假唤醒导致?
测试环境
- 操作系统:Windows
- 编译器:Clang、MSVC
复现代码
CVSimple.h
#include <condition_variable> #include <mutex> #include <queue> #include <vector> #include <iostream> class CVSimple { public: static void test() { std::queue<int> que; std::mutex m; std::condition_variable cv; int max_size = 10; bool working = true; auto producer = [&]() { while (working) { std::unique_lock<std::mutex> lock(m); std::chrono::milliseconds t(200); auto predicate = [&que, &max_size]() { if (que.size() < max_size) { std::cout << "T"; return true; } std::cout << "F"; return false; }; if (cv.wait_for(lock, t, predicate)) { std::cout << "+"; std::this_thread::sleep_for(std::chrono::milliseconds(50)); que.push(1); lock.unlock(); std::cout << "N"; cv.notify_one(); } else { //std::cout << "P"; } } }; auto consumer = [&]() { while (working) { std::unique_lock<std::mutex> lock(m); std::chrono::milliseconds t(200); auto predicate = [&que]() { if (!que.empty()) { std::cout << "t"; return true; } else { std::cout << "f"; return false; }; }; //cv.wait(lock, predicate); //std::cout << "-"; //std::this_thread::sleep_for(std::chrono::milliseconds(50)); //que.pop(); //lock.unlock(); //std::cout << "n"; //cv.notify_one(); if (cv.wait_for(lock, t, predicate)) { std::cout << "-"; std::this_thread::sleep_for(std::chrono::milliseconds(50)); que.pop(); lock.unlock(); std::cout << "n"; cv.notify_one(); } else { std::cout << "o"; } } }; int nprod = 100; int ncons = 1; std::cout << "Start producers" << std::endl; std::vector<std::thread> threads; for (int i = 0; i < nprod; ++i) { threads.emplace_back(producer); } std::cout << "Start consumers" << std::endl; for (int i = 0; i < ncons; ++i) { threads.emplace_back(consumer); } std::this_thread::sleep_for(std::chrono::seconds(20)); std::cout << "Stop working" << std::endl; working = false; for (auto& th : threads) { if (th.joinable()) { th.join(); } } return; } };
main.cpp
#include "CVSimple.h" int main() { CVSimple::test(); }
输出示例
Start producers T+Start consumers NT+NT+NT+NT+NT+NT+NT+NT+NT+NFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFt-nT+NFFFFFFFFFFt-nT+NFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
解答
1. 关于notify_one的标准规定
C++标准没有任何规定要求notify_one优先唤醒谓词结果为真的线程。notify_one的行为是从当前等待该条件变量的线程集合中,选择一个线程进行唤醒——选择逻辑是实现定义的(可能是随机、FIFO或其他顺序),完全不考虑线程等待时绑定的谓词。
谓词的检查是线程被唤醒后,在持有锁的前提下自行执行的步骤,这也是为什么wait/wait_for必须搭配谓词的核心原因:防止线程被唤醒后,条件仍不满足导致错误的业务操作。
2. 无渭词的消费者代码“正常工作”的原因
这只是测试中的巧合,本质上是不安全的写法。cv.wait(lock)没有谓词时,线程可能被虚假唤醒(这是条件变量的固有特性,标准允许实现产生虚假唤醒),此时队列可能为空,执行que.pop()会触发未定义行为。
你当前测试中未出现问题,要么是虚假唤醒未发生,要么是消费者被唤醒时队列恰好非空,但这不是可靠的行为。正确的写法必须搭配谓词,确保只有当队列非空时才继续执行后续操作。
3. 队列满后大量谓词检查的原因
这不是虚假唤醒导致的,而是因为你使用了cv.wait_for(lock, t, predicate)。该函数的逻辑是:如果谓词初始不满足,线程会进入等待,但每隔t时间就会自动醒来,重新检查谓词——无论是否有notify_one调用。
当队列满时,生产者的谓词返回false,wait_for会等待200毫秒后唤醒线程,再次检查谓词并输出F,然后继续等待,循环往复,这就是你看到大量F输出的原因。此外,当一个生产者被notify_one唤醒但谓词不满足时,它会重新进入等待,但其他生产者的wait_for定时到期会不断醒来检查谓词,因此会持续看到F。
如果将wait_for替换为wait(lock, predicate),线程只会在被notify或虚假唤醒时才会醒来检查谓词,此时队列满后生产者会进入等待,直到消费者操作后唤醒它们。
内容的提问来源于stack exchange,提问作者Piwniczne

