
基本用法
条件变量作为生产中者消费者模型中的典型临界区控制方案,在写代码过程中还是经常会使用到的,涉及到多线程总是会有很多坑,这里总结下 C++ 中条件变量 std::condition_variable。cppreference 给出的官方解释:
A condition variable is an object able to block the calling thread until notified to resume. It uses a unique_lock (over a mutex) to lock the thread when one of its wait functions is called. The thread remains blocked until woken up by another thread that calls a notification function on the same condition_variable object.
大概是说:条件变量是一个能够阻塞调用线程,直到被通知恢复的对象。它使用 unique_lock(基于 mutex)来锁定线程,当调用它的等待函数时,线程会被阻塞。线程会一直处于阻塞状态,直到另一个线程调用同一条件变量对象上的通知函数,将其唤醒。
核心接口
- wait:等待直到被唤醒,会阻塞当前线程直到被唤醒。
- wait_for:等待直到超时或者被唤醒。
- wait_until:等待直到到达某一时刻或者被唤醒。
示例代码
// condition_variable example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id (int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
// ...
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
int main ()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(print_id,i);
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto& th : threads) th.join();
return 0;
}
基本逻辑比较简单,主线程创建了十个子线程,用于执行 print_id 函数过程,print_id 中根据 ready 变量的状态决定是否等待,go 函数中改变 ready 状态并唤醒等待中的线程。
值得讨论的几个问题
-
notify_one 是不是一定只会唤醒一个阻塞线程?
顾名思义,很容易认为 notify_one 就是唤醒一个线程,但是查阅资料显示,并不一定保证只唤醒一个阻塞线程。

图中显示 pthread_cond_signal() 函数应当解锁至少一个在指定条件变量 cond 上被阻塞的线程(如果有线程在 cond 上被阻塞的话),也就是并不保证有且仅有一个线程被唤醒。具体可以参考:https://linux.die.net/man/3/pthread_cond_signal -
虚假唤醒问题
虚假唤醒和唤醒丢失问题核心都在while (!ready) cv.wait(lck);这行代码上,虚假唤醒存在于这里的 while 替换成 if 的场景,当 wait 阻塞线程后,条件变量满足条件发出唤醒信号,如果不止一个阻塞线程被唤醒,那么可能造成虚假唤醒,如下面代码。
void Producer()
{
std::this_thread::sleep_for(2000ms);
std::cout << "Ready Send notification." << std::endl;
mtx.lock();
q.push("message");
mtx.unlock();
cv.notify_all();
}
void Consumer()
{
std::cout << "Wait for notification." << std::endl;
std::unique_lock<std::mutex> lck(mtx);
if(q.empty())
cv.wait(lck);
std::string msg = q.front();
q.pop();
mtx.unlock();
std::cout << "Get: " << msg <<std::endl;
}
这里 while 被替换成了 if(q.empty()),当多个线程被同时唤醒,而消息队列里面只有一个消息,那么只有一个线程能处理这个消息,其余线程处理过程中,消息队列都为空,就出现了虚假唤醒,此时应该将 if 替换为 while,就不会导致其他线程错误进入处理状态,而是继续等待。
-
唤醒丢失问题
唤醒丢失问题是指没有 while 这一行判断的情况下,如果 wait 线程还没有走到 wait 逻辑,notify 就触发了,后续进行 wait 永远都不会收到 notify 信号,导致卡死。解决方法也比较简单,需要构造条件,当满足条件时才进行 wait 等待。例如上面例子中的 ready 变量就起到这个作用,即使 notify 先触发了,ready 也会被设置为 true,这样等待线程不会执行 wait 逻辑,也就不会卡死。 -
为什么条件变量一定要和锁绑定?
第一次加锁就是一个简单的临界区保护,条件变量需要传入一个 std::unique_lock 类型作为参数,是因为在 wait 执行后需要释放锁,让其它线程能进入临界区,比如 notify 线程。在 wait 结束后,需要再次尝试获取锁,如果没有这个过程,可能导致多个线程同时进入临界区,因为有多个线程可能被同时唤醒。
关注公众号:C++学习与探索,有惊喜哦~
1217

被折叠的 条评论
为什么被折叠?



