admin管理员组文章数量:1599529
一、什么是条件变量?
条件变量类(condition_variable)是一个同步原语,它可以在同一时间阻塞一个线程或者多个线程,直到其他线程改变了共享变量(条件)并通知。
primitive 原语,表达的是基础、基本的,是其他复杂应用的构建基础。
二、为什么需要条件变量?
- 减少轮询从而提高效率。没有条件变量,CPU会浪费时间反复轮询某一个条件。条件变量出现使得线程可以在不满足条件进行休眠,将资源让给有需要的其他线程;
- 线程之间的协调。两个线程之间执行可以变得有序,也就是一个线程执行完成后,另外一个线程才会执行;
- 复杂同步条件。有时候一个线程不仅需要知道资源可达,还需要知道一些额外信息,比如说生产消费者中,缓冲队列是否满之类的操作;
线程进入阻塞可以是sleep,也可以是wait。sleep期间所持有的锁不会释放,到时见会自动唤醒,而wait期间将会释放锁,唤醒还需要额外的条件。前者更多的用在模拟操作延迟,后者用于多线程协作场景。
试想一个场景,两个线程th1和th2,他们分别执行这两个不同的变量,th1的继续执行必须等到th2完成其计算过程才可以继续执行,那么将会有两种策略:
- 第一种,轮询th2是否执行标志位flag,th1种不断轮询标志位是否为true;
- 第二种,th1和th2通过条件变量进行联系,th1根据th2是否通知决定其是等待还是继续执行;
第一种方法,最简单的实现方法就是:
void th1_fun()
{
while(!flag)
{
std::this_thread::sleep_for(10ms);
}
//continue execute th1
}
检测到flag为false,线程进入休眠;检测到flag为true,退出while阻塞,继续执行,这个线程休眠是为了让其他线程有机会执行,也就是有机会让flag变成true,休眠时间如果过长,会导致响应延迟,如果休眠时间过短,cpu时间将会大部分浪费在判断flag是否改变。
第二种方法就是这里提到的条件变量,条件变量和轮询不一样的地方在于,他是用等待替代休眠的,这也就是说cpu不需要重复判断条件变量是否成立,而是通过另一个线程主动通知的方式告知无需继续等待。
三、std::condition_variable
头文件#include <condition_variable>
3.1 条件变量是如何完成同步的?
- 一个线程完成对数据的修改,另一个线程进行等待
- 数据修改完成,通知另一个线程已经完成修改
- 等待的线程收到通知,继续进行线程的执行
我们把对数据进行修改的线程称为通知线程,等待通知的线程称为等待线程。
3.2 通知端的具体工作
- 获取一个
std::mutex
防止变量写冲突(通常是std::lock_guard
); - 对变量值进行修改;
- 对条件变量调用
notify_one
或者notify_all
通知(通知的时候不一定要处于锁定);
3.3 等待端的具体工作
- 获得一个
std::unique_lock<std::mutex>
,这个互斥锁与通知端的相同; - 条件变量执行
wait
wait_for
或wait_until
,等待操作将原子地完成释放互斥锁和挂起线程操作; - 当条件变量被通知时,超时或者虚假唤醒,线程将会被唤醒,互斥锁再次被获取,线程需要检查条件和假如时虚假唤醒则继续等待。
!!std::condition_variable
只可与 std::unique_lock<std::mutex>
一同使用;此限制能让这种机制在一些平台取得最高的效率。当然如果你想使用与锁类型无关的条件变量,可以试试他的兄弟::std::condition_variable_any
。注意等待端释放和挂起是原子操作,具体原因点这里。
3.4 成员函数
构造函数只有无参默认构造函数,无拷贝、无赋值。剩下的就是两类成员函数,一是通知方法,二是等待方法。
-
通知方法:要么通知所有人
notify_one
,要么通知一个人notify_all
。 -
等待方法:等待wait、等待一段时间wait_for和等待某个时刻wait_until。使用的方式就是“条件变量调用某种方法等待unique_lcok”
cond.wait(ulo);
调用wait方法导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。为了避免虚假唤醒,你可以使用谓词:
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
等价于以下语句:
while (!pred()) {//不满足继续等待
wait(lock);
}
这个谓词应该理解为解除阻塞条件。
四、条件变量实例
这个例子实现了按序打印并发线程:
class ConditionVarTest
{
public:
void printOne()
{
for(int i=0;i<10;i++)
{
std::this_thread::sleep_for(1ms); //必要,因为可能消费者没有处理完数据就已经生产了下一个数据,并下发了另一个通知。
m_i=i;
dataReady=true;
cond.notify_all();
}
}
void printTwo()
{
for(int i=0;i<10;i++)
{
std::unique_lock<std::mutex> ulo(lo);
cond.wait(ulo,[this]{return dataReady==true;});
std::cout<<"Get data value = "<<m_i<<std::endl;
dataReady=false;
}
}
std::mutex lo;
std::condition_variable cond;
private:
bool dataReady=false;
int m_i=0;
};
int main()
{
ConditionVarTest v;
std::thread th1(&ConditionVarTest::printOne,&v);
std::thread th2(&ConditionVarTest::printTwo,&v);
th1.join();
th2.join();
}
五、虚假唤醒
条件变量还应该注意的地方在于虚假唤醒,也就是生产者发了通知,但是没有产品。
这常常出现在生产-消费设计模式中,假设消费者处理速度足够快,一个生产者一次只生产一个商品,有以下结论:
- 一个生产者,一个消费者,notify_one notify_all都不会出现虚假唤醒。此时notify_one等于notify_all,只通知一个自然不会出现虚假唤醒;
- 一个生产者,多个消费者,notify_one不会出现虚假唤醒,notify_all会出现。消费者可能同时收到商品到货通知,但是货物只可能有一个,其他消费者被唤醒,但是没有商品可供消费;
- 多个生产者,一个消费者,notify_one notify_all都不会出现虚假唤醒。此时notify_one等于notify_all,你多次通知给同一个消费者,消费者中总是有商品可供消费;
- 多个生产者,多个消费者,notify_one notify_all都会出现虚假唤醒。多个生产者发出notify_one,如果一个消费者处理完所有货物(黄牛),其他消费者将无货可用;多个生产者发出notify_all更加是如此。
小结:如果消费者只有一个,那么无论如何都不可能出现虚假唤醒;多生产者,多消费者,无论如何都会出现虚假唤醒;生产者只有一个,消费者多个,notify_one不会,而notify_all会出现。
如何处理虚假唤醒?在消费者唤醒之后,利用while反复对产品可用性进行检查,如果不可用继续进行wait进入等待唤醒状态。当然,C++11已经帮你处理好这种情况了,也就是wait的第二个参数。
[1] https://www.jianshu/p/01ad36b91d39
[2] https://blog.csdn/shizheng163/article/details/83661861
[3] https://www.zhihu/question/42962803/answer/120217624
[4] https://en.cppreference/w/cpp/thread/condition_variable
[5] https://zhuanlan.zhihu/p/422670024
20211021 修改了例子,和虚假唤醒部分描述。
20211029 线程阻塞有两种情况,一个是休眠,另一个是挂起。休眠是在特时刻恢复运行,挂起是等待某个条件后恢复运行。休眠不知道继续执行的具体时间,挂起则切确知道回复运行的时刻。
本文标签: 线程变量条件conditionvariables
版权声明:本文标题:C++11线程库 (六) 条件变量 Condition variables 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dongtai/1728322276a1153980.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论