前几篇介绍了使用mutex互斥体实现线程同步,操作系统中另外一个重要的事情就是线程之间实现通信。线程之间通信包含以下两种情形:
线程之间可以通信 即线程A B间的数据可以相互传递
。A 与B线程之间是有先后的执行顺序的,一种情况是A线程在完成某个操作之后再运行B线程
。
上述两种情况,使用互斥体mutex就可能显得不是很合适了,因为mutex的目的是为了以一种更加安全的方式去访问共享的变量,但无法做到线程间的数据通信或者实现线程之间逻辑的关系。因此引入新的控制同步的条件变量
C++11中是直接将Linux的机制封装,此处先讲在C语言中是如何实现的,再对C++中如何封装进行讲述,最后讲条件变量的使用场合。
1. 举例1:线程间存在先后的执行顺序和共享变量存在的问题
下例为对一个公共的string变量在线程A和B中进行传递,预期逻辑 A先传递信息给字符串,字符串信息后传递到B
。
//#include "stdafx.h"
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <future>
using namespace std;
//全局互斥体,用于同步
std::mutex g_mtx;
//用于发送消息
std::string str;
//预期逻辑:A传递信息给字符串,字符串信息传递到B
//B 子线程回调函数
void worker_thread()
{
printf("%s",str.c_str());
}
//A 主线程
int main()
{
//创建B线程,随时可能执行,
//很可能A还没有赋值B已经跑起来了
std::thread thd(worker_thread);
//使当前主线程睡眠1秒钟,使得CPU有机会切换执行子线程
std::this_thread::sleep_for(std::chrono::seconds(1));
str = "hello world";
thd.join();
}
运行结果:主线程中回调用子线程的回调函数,在创建了B线程之后,随时可能切换到B线程中,导致还来不及赋值就跳转到B线程,导致打印出现问题(什么都没输出)。
2. 举例2:使用锁以轮询方式解决线程间存在先后的执行顺序和共享变量的问题
为了解决上面的问题:使用锁保证共享变量的同步操作,利用标志位g_isSe
t和while(1)
进行线程A和B是否完成赋值的检查,具体代码如下:
//#include "stdafx.h"
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <future>
using namespace std;
//全局互斥体,用于同步
std::mutex g_mtx;
//用于发送消息
std::string str;
//增加标志位
bool g_isSet = false;
//预期逻辑 A先传递信息给字符串,字符串信息再传递到B
//问题:
//1.反应迟钝,等待时间长
//2.B线程不断循环(轮询),条件不成功,则休眠等待
//B 子线程回调函数
void worker_thread()
{
//利用while(true)死循环不断查看标志位状态
while (true)
{
std::lock_guard<std::mutex> lk(g_mtx);
//判断标志位
if (g_isSet)
{
printf("%s", str.c_str());
break;
}
else {
//条件未满足将子线程休息一会再循环
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
}
//A 主线程
int main()
{
//创建B线程,随时可能执行,
//很可能A还没有赋值B已经跑起来了
std::thread thd(worker_thread);
{
//由于使用了共享变量,因此需要进行同步设置
std::lock_guard<std::mutex> lk(g_mtx);
//使当前主线程睡眠1秒钟,使得CPU可切换执行子线程
std::this_thread::sleep_for(std::chrono::seconds(1));
str = "hello world";
//设置完成后标志位为true
g_isSet = true;
}
thd.join();
}
运行结果如下:
虽然上面的代码解决了对公有变量操作问题,但是等待时间过长的问题,这是由于B线程不断循环,条件不成功,则休眠等待
。
整个过程简单描述: 主线程A比作快递员送快递,子线程B比作我们本身取快递,比较快捷的方式就是快递到了之后,快递员告知我们去取,上面的代码就变成了我们不知道快递什么时候到,我们就不断地去查看快递是否到货,这种方式就是轮询
,如果没到我们休息一会再去查看。
3. 条件变量的引入
轮询的方式十分低效,实际采用快递到货后通知是最高效的,这种在计算机中称为事件模型,通知方式
,对于上例中需要达到的效果:主线程A在完成字符串信息传递之后,发送消息给线程B,此时再执行线程B中的操作
这种经典方式在操作系统(R3&R0)内部有大量使用,Windows内部有大量的event事件(Qt中的event应该也是采用此种方式),网络中iocp epoll kevent都可以使用事件的模式操作
这种思想可以使用条件变量来完成当前多线程通信或者说线程先后顺序执行,下篇将会讲条件变量是如何完成事件模型的
。
4. 学习视频地址:条件变量的引入