C++11线程支持库
文章目录
- C++11线程支持库
- 进程与线程的基本概念
- 线程的创建
- 普通函数
- 函数指针
- 函数引用(了解)
- 函数对象
- lambda表达式
- function对象
- 传递bind绑定的函数
- C++11中的锁
- mutex锁
- lock_guard
- unique_lock
- atomic类型
- 生产者与消费者模型
- 原理图
- 代码实现
- 程序架构
- 代码演示
- 运行效果
进程与线程的基本概念
进程(Process)
操作系统资源分配的基本单位
本质:一个正在运行的程序实例,是操作系统分配内存、文件描述符等资源的基本单位。
核心特征
独立资源:每个进程有自己独立的内存空间、代码段、数据段,进程间默认互不干扰(进程间通信需借助管道、socket、共享内存等机制)。
独立运行:进程是操作系统调度的独立实体,一个进程崩溃不会影响其他进程(隔离性强)。
开销较大:创建、销毁、切换进程的开销高(需分配 / 释放资源、切换内存映射)。举例:用户打开的浏览器、VS、终端,各自都是一个独立的进程。
线程(Thread)
操作系统CPU调度的基本单位
本质::进程内的执行单元,也叫 “轻量级进程”。一个进程可以包含多个线程,所有线程共享该进程的全部资源。
核心特征
资源共享:同一进程的所有线程共享进程的内存、文件句柄、全局变量等资源;线程仅独有自己的栈空间、寄存器状态(线程上下文)。
调度轻量化:CPU 调度的基本单位是线程,线程的创建、切换、销毁开销远小于进程。
协同工作:多线程通过共享资源协同完成任务,但需用互斥锁、条件变量等工具解决数据竞争问题。
依赖性:线程不能独立存在,必须依附于进程;进程退出时,其所有线程会被强制终止。举例:浏览器进程中,“渲染页面”“下载文件”“处理用户输入” 分别由不同的线程负责。
使用多线程的意义
不考虑单线程异步模型的情况下,如果只使用单进程单线程处理问题,那么所有的任务都是串行执行的(一个接一个执行);
如果使用了多线程处理问题,那么任务是并发执行的(一段时间内,多个任务同时执行——宏观上同时,微观上不一定同时)。如果机器是单核的,这些任务看起来是同时执行的,实际上底层依靠CPU调度策略轮流使用一定的时间片;如果机器是多核的,那么这些任务真的会有同时执行的机会,也就是并行执行(真正意义上的,同一时刻多个任务同时执行)。利用多核,可以提升计算速度。
另外,使用多线程还可以避免界面“卡死”,提升响应速度——比如在图形界面(GUI)程序里点击了一个“下载大文件”的按钮。如果放在单线程里,程序会一直卡在下载循环中,界面无法刷新、无法拖动,看起来像“死机”了。多线程的核心价值在于,可以让主线程(UI线程)专门负责响应用户点击和绘制界面,而让子线程去干下载这种需要等待(I/O阻塞)的脏活累活。在这种场景下,哪怕机器是单核的,多线程也是必要的,因为它让程序“动”起来了。
线程的创建
对于早期的C++语言而言,如果想使用线程,我们需要根据不同的平台使用不用的接口,比如:在Linux平台上,我们需要借助POSIX标准的线程库,在windows上需要借助windows线程库,因为C++自己没有独立的线程库。
为了解决这个问题,在C++11标准中,做了完善,C++自己引入了与平台无关的线程库,这个库是语言层面的库,这就是C++11线程库。
线程的构造函数
thread()noexcept;thread(thread&&other)noexcept;template<classFunction,class...Args>explicitthread(Function&&f,Args&&...args);thread(constthread&)=delete;第一种形式,可以创建一个空的线程对象,但是线程创建出来之后,需要做任务,单独使用这种形式没有意义;
第二种形式,可以从另外一个线程对象转移过来;
第三种形式,传递任何可调用实体的形式,这种形式使用的最为通用;(此形式与std::bind的形式一样)
可调用实体是指所有能够通过函数调用语法 () 来执行的实体。比如函数,仿函数,匿名函数,bind返回的对象等
第四种形式,表明线程对象不能进行复制。每个线程的线程id是唯一的。
我们重点研究第三种形式。
普通函数
按照参考文档的说明,尝试创建子线程,并由子线程调用threadFunc函数
出现错误
我们先抛开报错,我们从运行的逻辑来讲,应该是从上至下的依次运行,但是很明显,这里打印了test()函数的id,但是按道理来讲应该是先运行创建的线程,也就是threadfunc()函数。这是为什么呢?而且出现了报错,这两者是不是有什么关系?
在这里我们就不能依靠之前的思维去理解程序运行的逻辑了,正如上面所说,线程是计算机调度的基本单元,我们创建了线程,就是告诉计算机,这个函数的调用时机由计算机自己决定。当然在我们的代码中还存在一种关系,就是threadfunc的线程是在test函数中创建的,所以有依赖的关系,出现这样的结果说明主线程可能跑得很快,而程序结束时子线程还没跑完,需要让主线程等待子线程的退出。
让主线程等待子线程之后,就不会出现报错了,另外就是可以从运行结果可以看到,他每次的运行结果不一定一致,所以这就体现了,由操作系统自行决定线程执行的顺序,也就是调度。
—— 如果想要看到主线程的线程id,可以在main函数中调用this_thread::get_id函数。
注意
这种线程的使用方式是属于语言层面上的,可以跨平台使用,只要平台支持C++语法,就可以生效。
而Linux阶段的诸如pthread_create等写法如果想要在windows平台使用就会有问题。所以C++所提供的这套写法的跨平台能力更强。
函数指针
创建thread时还可以传入函数指针。
函数引用(了解)
函数指针可以使用,还有一种用得较少的写法 —— 函数引用,其形式也非常简单
函数对象
lambda表达式
function对象
传递bind绑定的函数
bind函数的结果实际也还是function对象
C++11中的锁
mutex锁
互斥锁用于协调多个线程对共享资源的访问,作用是保证同一时刻只有一个线程可以访问共享资源,其他线程需要等待互斥锁释放后才能访问。在多线程编程中,多个线程可能同时访问同一个共享资源,如果没有互斥锁的保护,就可能出现数据竞争等问题。
互斥锁的概念并不陌生,在Linux下,POSIX标准中也有互斥锁的概念,这里我们说的互斥锁是C++11语法层面提出来的概念,是C++语言自身的互斥锁std::mutex,互斥锁只有两种状态:上锁与解锁。
#include<mutex>classmutex;构造函数
成员函数
看一个简单示例:
我们预想的情况应该是创建了两个线程,每个线程对全局变量a进行10000次++操作,结果应该是20000.但是很明显结果不符合我们的预期,并且从运行结果上可以发现每次的运算结果都是不同。
这就涉及到了共享资源访问的问题,很明显th1和th2都对变量a进行了数据修改,对于变量a的访问大致过程是这样的
- 访问变量a的地址读取数据
- 将数据写入寄存器等待cpu进行++操作
- cpu运算之后将数据放在寄存器
- 读取寄存器的值写回内存中
因为线程的执行是由cpu自行调度的,所以会出现,当一个线程执行的时候数据还没被写回到内存时就被另外一个线程取走了。第二个线程计算的数据仍然是原数据。所以就会出现以上的情况。
所以对于共享资源的访问,我们可以通过锁来进行处理,在对共享变量的操作访问结束之前,不允许其他线程进行访问。具体代码如下
上锁之后一定要记住解锁,不然会出现锁死的情况。
针对于可能忘记解锁的情况我们可以做一个简单的优化,就像只能指针一样,使用RAII的思想,我们只用它来加锁,不想考虑什么时候解锁。同样也是通过析构函数,然后将锁对象托管给栈对象。
lock_guard
对于std::mutex互斥锁而言,必须手动上锁与解锁,而且必须成对出现。如果上锁了,但是由于某些原因没有解锁,就会导致程序一直处于锁定状态而无法解锁。在C++11中使用了C++之父提出来的思想RAII,设计了两种锁std::lock_guard与std::unique_lock。首先来看看lock_guard:
头文件
构造函数
替换上例中的自定义类型
注意:该类没有提供其他的成员函数,也就是说并没有提供人为上锁lock与解锁unlock的功能,这导致了使用的灵活性不够。
也就意味着如果该函数中还有其它非共享资源访问的操作,依然要等待函数执行完,才能让下一个线程访问共享资源,即使当前线程已经对共享资源访问结束。
就算使用块作用域的写法,也无法应对更灵活的使用需求 —— 引出另一种锁:unique_lock
unique_lock
相比较lock_guard而言,unique_lock提供了更多的成员函数,使用更加灵活,可以手动的加锁与解锁。
效果上有所区别:
unique_lock同样使用了RAII的思想
unique_lock比lock_guard更灵活,但是性能相对低一些。
总结:
1、lock_guard与unique_lock都使用了RAII的思想,利用栈对象的生命周期管理资源
2、lock_guard没有额外提供上锁与解锁的功能,不够灵活,但是unique_lock提供了。
3、lock_guard比unique_lock效率更高一些。
atomic类型
之前使用锁来管理是希望每次对gCnt的操作成为原子操作,C++11同样提供了更直接的方式
整型数据都可以用
如果是自定义类型,不见得总是原子数据类型,可以使用is_lock_free函数进行判断
实现原理:
CAS机制:会将变量的预期值A与在内存中的值V进行比较,如果V与A是相等,就可以将该值改为B;如果V与A的值是不相等,那么就不能将其改为新值B。
生产者与消费者模型
生产者与消费者问题,是一个经典的常规问题,其实也是线程问题。
可以把生产者看成是一类线程,消费者看成是另一类线程,也就是C++11线程库中的std::thread。因为生产者与消费者需要从共享的仓库中存数据或者取数据,涉及到对仓库的互斥访问,所以需要加锁,也就是std::mutex。
这里我们把仓库用一个任务队列TaskQueue进行封装,提供互斥锁、条件变量的基本数据成员;以及判断队列是不是满的,是不是空的,存数据与取数据等基本操作。
原理图
生产者生产数据,消费者消费数据。
由于生产速度和消费速度的不匹配,以及可能会有多个消费者的情况,在这个场景下需要仓库作为缓冲区。
—— 问题:生产者生产数据是否应该直接往仓库传输呢?
不应该,无论是生产者还是消费者,在与仓库之间进行数据传输时,必然需要唯一的一把锁来确保数据不会紊乱。
—— 问题:如果生产者生产数据非常快,消费者消费数据非常慢,那么可能出现仓库满的情况,此时生产者继续生产数据应该怎么办呢?
仓库满时,利用条件变量让生产者线程睡眠,等消费者消费了数据,再唤醒生产者;同样的道理,如果仓库空了,也要利用条件变量让消费者线程睡眠。
如果生产者和消费者的速度是相同的,那直接让生产者与消费者一一对应就行,不需要仓库了。
C++中使用条件变量可以使用std::condition_variable
代码实现
在实现代码之前需要提醒一下,以上是我们的逻辑图,思路无疑是没有问题的,但是我们如果需要打印日志来验证我们的逻辑,这里会有一些问题,如果我们打印的话使用cout,那么输出缓冲区其实也是共享资源,所以在这里我们也应该纳入我们的考虑范围内。不然就会出现打印错乱的现象。只需要在打印日志的时候加一个专门的锁就好了,但是在这我想尝试一下另一种方法,就是在之前我在写stm32的项目时的启发,我们可以创建一个队列,将待打印的数据放入队列当中,输出任务就只需要从队列里边取数据输出就可以了,这样既可以解决打印错乱的问题,也可以正确的反应,各个线程真实的执行顺序。
那么我们在分析一下队列,就会发现实际上就会发现和仓库类的方法都是一样的,所以如果要使用一个类来控制输出的话,我们就是实现了两个生产者消费者模型
程序架构
代码演示
warehouse.h文件
#ifndef__WAREHOUSE_H#define__WAREHOUSE_H#include<iostream>#include<queue>#include<mutex>#include<condition_variable>usingstd::queue;usingstd::mutex;usingstd::condition_variable;usingstd::unique_lock;classWareHouse{public:// 构造函数WareHouse();WareHouse(size_t cap);// 析构函数~WareHouse();// 压入一个数据(生产者生产一个产品)voidpush(intval);// 弹出一个数据(消费者消费一个产品)intpop();// 判满boolfull();// 判空boolempty();private:size_t _capacity;queue<int>_que;mutex _mutex;condition_variable _notempty;condition_variable _notfull;};#endifwarehouse.cpp文件
#include"warehouse.h"#if0size_t _capacity;queue<int>_que;mutex _mutex;condition_variable _notempty;condition_variable _notfull;#endif// 构造函数WareHouse::WareHouse():WareHouse(10){}WareHouse::WareHouse(size_t cap):_capacity(cap),_que(),_mutex(),_notempty(),_notfull(){}// 析构函数WareHouse::~WareHouse(){}// 压入一个数据(生产者生产一个产品)voidWareHouse::push(intval){unique_lock<mutex>mtx(_mutex);// 队列满,阻塞生产while(full()){_notfull.wait(mtx);}_que.push(val);_notempty.notify_all();}// 弹出一个数据(消费者消费一个产品)intWareHouse::pop(){unique_lock<mutex>mtx(_mutex);while(empty()){_notempty.wait(mtx);}intres=_que.front();_que.pop();_notfull.notify_all();returnres;}// 判满boolWareHouse::full(){return_que.size()==_capacity;}// 判空boolWareHouse::empty(){return_que.size()==0;}messagequeue.h文件
#ifndef__message_queue_h#define__message_queue_h#include<iostream>#include<mutex>#include<queue>#include<condition_variable>#include<string>usingstd::cout;usingstd::endl;usingstd::queue;usingstd::string;usingstd::mutex;usingstd::lock_guard;usingstd::unique_lock;usingstd::condition_variable;classMessageQueue{public:MessageQueue();MessageQueue(size_t cap);boolisfull();boolisempty();voidpush_message(string msg);voidprint();private:size_t _cap;queue<string>_msg_que;mutex _mtx;condition_variable _not_full;condition_variable _not_empty;};#endifmessagequeue.cpp文件
#include"messagequeue.h"#if0size_t _cap;queue<string>_msg_que;mutex _mtx;condition_variable _not_full;condition_variable _not_empty;#endifMessageQueue::MessageQueue():MessageQueue(10){}MessageQueue::MessageQueue(size_t cap):_cap(cap),_msg_que(),_mtx(),_not_full(),_not_empty(){}boolMessageQueue::isfull(){return_msg_que.size()==_cap;}boolMessageQueue::isempty(){return_msg_que.size()==0;}voidMessageQueue::push_message(string msg){unique_lock<mutex>mtx(_mtx);while(isfull()){_not_full.wait(mtx);}_msg_que.push(msg);_not_empty.notify_all();}voidMessageQueue::print(){unique_lock<mutex>mtx(_mtx);while(isempty()){_not_empty.wait(mtx);}cout<<_msg_que.front()<<endl;cout<<"------------------------------"<<endl;_msg_que.pop();_not_full.notify_all();}producer.h文件
#ifndef__producer_h#define__producer_h#include"warehouse.h"#include"messagequeue.h"classProducer{public:Producer();Producer(inti);~Producer();voidproduce(WareHouse&wh,MessageQueue&mq);voidproduce(WareHouse&wh,MessageQueue&mq,intval);private:int_id;};#endifproducer.cpp文件
#include"producer.h"Producer::Producer():Producer(-1){}Producer::Producer(inti):_id(i){}Producer::~Producer(){}voidProducer::produce(WareHouse&wh,MessageQueue&mq){produce(wh,mq,-1);}voidProducer::produce(WareHouse&wh,MessageQueue&mq,intval){wh.push(val);stringmsg("<producer "+std::to_string(_id)+"> produce a "+std::to_string(val));mq.push_message(msg);}consumer.h文件
#ifndef__consumer_h#define__consemer_h#include"warehouse.h"#include"messagequeue.h"classConsumer{public:Consumer();Consumer(intid);~Consumer();voidconsume(WareHouse&wh,MessageQueue&mq);private:int_id;};#endifconsumer.cpp文件
umer::Consumer():Consumer(-1){}Consumer::Consumer(intid):_id(id){}Consumer::~Consumer(){}voidConsumer::consume(WareHouse&wh,MessageQueue&mq){intval=wh.pop();stringmsg("{consumer "+std::to_string(_id)+"} consume a "+std::to_string(val));mq.push_message(msg);}main.cpp文件
#include"consumer.h"#include"messagequeue.h"#include"producer.h"#include"warehouse.h"#include<thread>#include<chrono>usingstd::thread;usingstd::ref;usingstd::this_thread::sleep_for;voidproduce(intid,MessageQueue&mq,WareHouse&wh){Producerp(id);intn=id;while(1){p.produce(wh,mq,n++);sleep_for(std::chrono::microseconds(600));}}voidconsume(intid,MessageQueue&mq,WareHouse&wh){Consumerc(id);while(1){c.consume(wh,mq);sleep_for(std::chrono::microseconds(400));}}voidprint(MessageQueue&mq){while(1){mq.print();}}intmain(){MessageQueuemsg_que(10);WareHousewh(10);threadp1(produce,1,ref(msg_que),ref(wh));threadp2(produce,9,ref(msg_que),ref(wh));threadp3(produce,7,ref(msg_que),ref(wh));threadc1(consume,2,ref(msg_que),ref(wh));threadc2(consume,5,ref(msg_que),ref(wh));threadshow(print,ref(msg_que));p1.join();p2.join();p3.join();c1.join();c2.join();show.join();return0;}运行效果
因为我们没有设置终止条件,所以各个线程会一直运行下去,所以运行之后快速按ctrl+c终止观察即可。另外需要说一点的是,要在各个线程运行一次之后主动放弃运行,不然就会出现一个线程长时间霸占CPU的现象,这样显然是不合理的。
那么至此我们就实现了生产者-消费者模型。