接上篇:C++新特性35_条件变量的引入(传统采用while(1)的轮询方式解决线程按顺序执行和共享变量问题十分低效,使用条件变量可以实现高效的事件模型(类似于Qt中信号槽机制))引入了条件变量,用于线程间数据交互或者线程间存在先后顺序的情况,本篇将介绍如何使用条件变量。
条件变量在C++中有一个类,此处先不讲这个类,先讲在新的C++11中条件变量的用法
之后再讲C++11中是如何封装的 。
C++新特性36_条件变量的使用
1. 条件变量的用法
下为利用C++11中条件变量的代码:子线程先进入等待,主线程再发送信号
#include <iostream>
#include <string>
#include <mutex>
#include <thread>
/*
//条件变量 C11
_CRTIMP2_PURE int __cdecl _Cnd_init(_Cnd_t *); //初始化 _Cnd_t是作为条件变量对象
_CRTIMP2_PURE void __cdecl _Cnd_destroy(_Cnd_t); //销毁
_CRTIMP2_PURE int __cdecl _Cnd_wait(_Cnd_t, _Mtx_t);//等待
//条件变量是满足事件模型的,就会有通知和响应的过程
//_Cnd_broadcast所有等待的线程都得到了通知
_CRTIMP2_PURE int __cdecl _Cnd_broadcast(_Cnd_t);//广播
//只有某一个线程得到了通知
_CRTIMP2_PURE int __cdecl _Cnd_signal(_Cnd_t);//信号
//互斥体 C11
_CRTIMP2_PURE int __cdecl _Mtx_init(_Mtx_t *, int);
_CRTIMP2_PURE void __cdecl _Mtx_destroy(_Mtx_t);
_CRTIMP2_PURE void __cdecl _Mtx_init_in_situ(_Mtx_t, int);
_CRTIMP2_PURE void __cdecl _Mtx_destroy_in_situ(_Mtx_t);
_CRTIMP2_PURE int __cdecl _Mtx_current_owns(_Mtx_t);
_CRTIMP2_PURE int __cdecl _Mtx_lock(_Mtx_t);
_CRTIMP2_PURE int __cdecl _Mtx_trylock(_Mtx_t);
_CRTIMP2_PURE int __cdecl _Mtx_timedlock(_Mtx_t, const xtime *);
_CRTIMP2_PURE int __cdecl _Mtx_unlock(_Mtx_t);
*/
//全局互斥体,用于同步
//用于发送消息
std::string g_str;
bool g_isSet = false;
//条件变量对象
_Cnd_t g_cnd;
//互斥体
_Mtx_t g_mtx;
//条件变量
// A --> str
// B <-- str
//问题:
//1. 等待的时间过久?
// 轮循:循环
// 条件不成功,则休眠等待
//2. 希望得到通知
// 事件模型,通知方式
// 操作系统(R3 & R0) event事件
// 网络 iocp epoll kevent
// 使用条件变量来完成,当前的线程间通信
//B 子线程
// 利用条件变量,可以控制线程执行的逻辑顺序,也可以用来传递消息
void worker_thread() {
//std::this_thread::sleep_for(std::chrono::seconds(1));
_Mtx_lock(g_mtx);
//子线程接收通知
while (!g_isSet) {
//都是原子性:不可以被打断
//1. 等待g_cnd被通知
//2. g_mtx 会被释放
//3. 当等到了g_cnd被触发时,g_mtx会被重新获取
_Cnd_wait(g_cnd, g_mtx); //陷入等待的过程,等到信号
}
printf("%s\r\n", g_str.c_str());
_Mtx_unlock(g_mtx);
}
//A 主线程
int main() {
//初始化 对应了构造函数,需要使用的是一个指针 _Cnd_init(_Cnd_t *)
_Cnd_init(&g_cnd);
//选择普通互斥体类型
_Mtx_init(&g_mtx, _Mtx_plain);
//起一条子线程
std::thread thd(worker_thread);
//std::this_thread::sleep_for(std::chrono::seconds(1)); //主线程休眠就可以保证子线程先执行等待主线程的信号
//主线程发送信号(通知)
_Cnd_signal(g_cnd);
_Mtx_lock(g_mtx);
g_isSet = true;
g_str = "Hello World!";
_Mtx_unlock(g_mtx);
//表示主线程已经准备完毕,通知子线程开始处理
//主线程发送信号(通知)
_Cnd_signal(g_cnd);//给条件变量发信号
thd.join();
//析构条件变量
_Cnd_destroy(g_cnd);
_Mtx_destroy(g_mtx);
return 0;
}
其中对于_CRTIMP2_PURE int __cdecl _Mtx_init(_Mtx_t *, int);
中的第二个参数是前面讲到的互斥体的类型,普通、超时、递归等。
整体代码的思维导图如下:
2. 在正常代码中如何使用条件变量发送信号满足逻辑关系?
std::thread thd(worker_thread);
起一条子线程,可能在main之前或者之后调,操作系统采用动态的调用回调函数worker_thread()
,有可能是在发送信号前也有可能是在发送信号之后。
- 如果在发送信号之前调用子线程,
while (!g_isSet)中g_isSet=false
就执行_Cnd_wait(g_cnd, g_mtx);
进行等待, - 如果在发送信号之后调用子线程,当
while (!g_isSet)中g_isSet = true;
时,接受信号进行打印 - 子线程回调是在发送信号前还是在发送信号之后都不会有影响。
条件变量g_cnd一定要搭配互斥体g_mtx使用根本原因是他们在保护一种条件while (!g_isSet),条件变量g_cnd总是搭配一种条件使用,所以将因此将g_cnd
称为条件变量;
条件变量g_cnd要被两个线程共享使用,所以需要依赖互斥体,因此条件while (!g_isSet)、 条件变量g_cnd、 互斥体g_mtx三胞胎需要同时使用。
3. 总结
-
条件变量:用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知。
-
条件变量自身并不包含条件。因为它通常和 if (或者while) 一起用,所以叫条件变量
-
需要配合互斥体mtx一起使用
-
使用:
-
_Cnd_init/_Cnd_destroy
-
_Cnd_wait :
- 进入函数前,必须首先获得mtx锁
- 进入该函数中,线程阻塞,发生:
- 线程陷入等待,直至条件变量信号被触发
- 互斥体mtx解锁
- 线程等待条件变量信号成功,函数即将返回,发生:
- 在函数返回之前,mtx再次被上锁
-
_Cnd_signal:
- 解除当前在条件变量上等待的一个线程。 如果没有线程被阻塞,则不执行任何操作并返回。
-
_Cnd_broadcast:
- 解除当前等待条件变量的所有线程。 如果没有线程被阻塞,则不执行任何操作并返回。
3.学习视频地址: 条件变量的使用
4. 问题
请你说说条件变量?
得分点
线程同步、阻塞、唤醒
标准回答
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。 使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的。如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作: 唤醒 再次获取互斥锁 重新评估条件