C++学习(11) ——C++11线程库(生产者消费者模型实现)

📅 发布时间:2026/7/16 4:20:25
C++学习(11) ——C++11线程库(生产者消费者模型实现) C11线程支持库文章目录C11线程支持库进程与线程的基本概念线程的创建普通函数函数指针函数引用了解函数对象lambda表达式function对象传递bind绑定的函数C11中的锁mutex锁lock_guardunique_lockatomic类型生产者与消费者模型原理图代码实现程序架构代码演示运行效果进程与线程的基本概念进程Process操作系统资源分配的基本单位本质一个正在运行的程序实例是操作系统分配内存、文件描述符等资源的基本单位。核心特征独立资源每个进程有自己独立的内存空间、代码段、数据段进程间默认互不干扰进程间通信需借助管道、socket、共享内存等机制。独立运行进程是操作系统调度的独立实体一个进程崩溃不会影响其他进程隔离性强。开销较大创建、销毁、切换进程的开销高需分配 / 释放资源、切换内存映射。举例用户打开的浏览器、VS、终端各自都是一个独立的进程。线程Thread操作系统CPU调度的基本单位本质:进程内的执行单元也叫 “轻量级进程”。一个进程可以包含多个线程所有线程共享该进程的全部资源。核心特征资源共享同一进程的所有线程共享进程的内存、文件句柄、全局变量等资源线程仅独有自己的栈空间、寄存器状态线程上下文。调度轻量化CPU 调度的基本单位是线程线程的创建、切换、销毁开销远小于进程。协同工作多线程通过共享资源协同完成任务但需用互斥锁、条件变量等工具解决数据竞争问题。依赖性线程不能独立存在必须依附于进程进程退出时其所有线程会被强制终止。举例浏览器进程中“渲染页面”“下载文件”“处理用户输入” 分别由不同的线程负责。使用多线程的意义不考虑单线程异步模型的情况下如果只使用单进程单线程处理问题那么所有的任务都是串行执行的一个接一个执行如果使用了多线程处理问题那么任务是并发执行的(一段时间内多个任务同时执行——宏观上同时微观上不一定同时)。如果机器是单核的这些任务看起来是同时执行的实际上底层依靠CPU调度策略轮流使用一定的时间片如果机器是多核的那么这些任务真的会有同时执行的机会也就是并行执行真正意义上的同一时刻多个任务同时执行。利用多核可以提升计算速度。另外使用多线程还可以避免界面“卡死”提升响应速度——比如在图形界面GUI程序里点击了一个“下载大文件”的按钮。如果放在单线程里程序会一直卡在下载循环中界面无法刷新、无法拖动看起来像“死机”了。多线程的核心价值在于可以让主线程UI线程专门负责响应用户点击和绘制界面而让子线程去干下载这种需要等待I/O阻塞的脏活累活。在这种场景下哪怕机器是单核的多线程也是必要的因为它让程序“动”起来了。线程的创建对于早期的C语言而言如果想使用线程我们需要根据不同的平台使用不用的接口比如在Linux平台上我们需要借助POSIX标准的线程库在windows上需要借助windows线程库因为C自己没有独立的线程库。为了解决这个问题在C11标准中做了完善C自己引入了与平台无关的线程库这个库是语言层面的库这就是C11线程库。线程的构造函数thread()noexcept;thread(threadother)noexcept;templateclassFunction,class...Argsexplicitthread(Functionf,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对象C11中的锁mutex锁互斥锁用于协调多个线程对共享资源的访问作用是保证同一时刻只有一个线程可以访问共享资源其他线程需要等待互斥锁释放后才能访问。在多线程编程中多个线程可能同时访问同一个共享资源如果没有互斥锁的保护就可能出现数据竞争等问题。互斥锁的概念并不陌生在Linux下POSIX标准中也有互斥锁的概念这里我们说的互斥锁是C11语法层面提出来的概念是C语言自身的互斥锁std::mutex互斥锁只有两种状态上锁与解锁。#includemutexclassmutex;构造函数成员函数看一个简单示例我们预想的情况应该是创建了两个线程每个线程对全局变量a进行10000次操作结果应该是20000.但是很明显结果不符合我们的预期并且从运行结果上可以发现每次的运算结果都是不同。这就涉及到了共享资源访问的问题很明显th1和th2都对变量a进行了数据修改对于变量a的访问大致过程是这样的访问变量a的地址读取数据将数据写入寄存器等待cpu进行操作cpu运算之后将数据放在寄存器读取寄存器的值写回内存中因为线程的执行是由cpu自行调度的所以会出现当一个线程执行的时候数据还没被写回到内存时就被另外一个线程取走了。第二个线程计算的数据仍然是原数据。所以就会出现以上的情况。所以对于共享资源的访问我们可以通过锁来进行处理在对共享变量的操作访问结束之前不允许其他线程进行访问。具体代码如下上锁之后一定要记住解锁不然会出现锁死的情况。针对于可能忘记解锁的情况我们可以做一个简单的优化就像只能指针一样使用RAII的思想我们只用它来加锁不想考虑什么时候解锁。同样也是通过析构函数然后将锁对象托管给栈对象。lock_guard对于std::mutex互斥锁而言必须手动上锁与解锁而且必须成对出现。如果上锁了但是由于某些原因没有解锁就会导致程序一直处于锁定状态而无法解锁。在C11中使用了C之父提出来的思想RAII设计了两种锁std::lock_guard与std::unique_lock。首先来看看lock_guard头文件构造函数替换上例中的自定义类型注意该类没有提供其他的成员函数也就是说并没有提供人为上锁lock与解锁unlock的功能这导致了使用的灵活性不够。也就意味着如果该函数中还有其它非共享资源访问的操作依然要等待函数执行完才能让下一个线程访问共享资源即使当前线程已经对共享资源访问结束。就算使用块作用域的写法也无法应对更灵活的使用需求 —— 引出另一种锁unique_lockunique_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的操作成为原子操作C11同样提供了更直接的方式整型数据都可以用如果是自定义类型不见得总是原子数据类型可以使用is_lock_free函数进行判断实现原理CAS机制会将变量的预期值A与在内存中的值V进行比较如果V与A是相等就可以将该值改为B如果V与A的值是不相等那么就不能将其改为新值B。生产者与消费者模型生产者与消费者问题是一个经典的常规问题其实也是线程问题。可以把生产者看成是一类线程消费者看成是另一类线程也就是C11线程库中的std::thread。因为生产者与消费者需要从共享的仓库中存数据或者取数据涉及到对仓库的互斥访问所以需要加锁也就是std::mutex。这里我们把仓库用一个任务队列TaskQueue进行封装提供互斥锁、条件变量的基本数据成员以及判断队列是不是满的是不是空的存数据与取数据等基本操作。原理图生产者生产数据消费者消费数据。由于生产速度和消费速度的不匹配以及可能会有多个消费者的情况在这个场景下需要仓库作为缓冲区。—— 问题生产者生产数据是否应该直接往仓库传输呢不应该无论是生产者还是消费者在与仓库之间进行数据传输时必然需要唯一的一把锁来确保数据不会紊乱。—— 问题如果生产者生产数据非常快消费者消费数据非常慢那么可能出现仓库满的情况此时生产者继续生产数据应该怎么办呢仓库满时利用条件变量让生产者线程睡眠等消费者消费了数据再唤醒生产者同样的道理如果仓库空了也要利用条件变量让消费者线程睡眠。如果生产者和消费者的速度是相同的那直接让生产者与消费者一一对应就行不需要仓库了。C中使用条件变量可以使用std::condition_variable代码实现在实现代码之前需要提醒一下以上是我们的逻辑图思路无疑是没有问题的但是我们如果需要打印日志来验证我们的逻辑这里会有一些问题如果我们打印的话使用cout那么输出缓冲区其实也是共享资源所以在这里我们也应该纳入我们的考虑范围内。不然就会出现打印错乱的现象。只需要在打印日志的时候加一个专门的锁就好了但是在这我想尝试一下另一种方法就是在之前我在写stm32的项目时的启发我们可以创建一个队列将待打印的数据放入队列当中输出任务就只需要从队列里边取数据输出就可以了这样既可以解决打印错乱的问题也可以正确的反应各个线程真实的执行顺序。那么我们在分析一下队列就会发现实际上就会发现和仓库类的方法都是一样的所以如果要使用一个类来控制输出的话我们就是实现了两个生产者消费者模型程序架构代码演示warehouse.h文件#ifndef__WAREHOUSE_H#define__WAREHOUSE_H#includeiostream#includequeue#includemutex#includecondition_variableusingstd::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;queueint_que;mutex _mutex;condition_variable _notempty;condition_variable _notfull;};#endifwarehouse.cpp文件#includewarehouse.h#if0size_t _capacity;queueint_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_lockmutexmtx(_mutex);// 队列满阻塞生产while(full()){_notfull.wait(mtx);}_que.push(val);_notempty.notify_all();}// 弹出一个数据消费者消费一个产品intWareHouse::pop(){unique_lockmutexmtx(_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#includeiostream#includemutex#includequeue#includecondition_variable#includestringusingstd::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;queuestring_msg_que;mutex _mtx;condition_variable _not_full;condition_variable _not_empty;};#endifmessagequeue.cpp文件#includemessagequeue.h#if0size_t _cap;queuestring_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_lockmutexmtx(_mtx);while(isfull()){_not_full.wait(mtx);}_msg_que.push(msg);_not_empty.notify_all();}voidMessageQueue::print(){unique_lockmutexmtx(_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#includewarehouse.h#includemessagequeue.hclassProducer{public:Producer();Producer(inti);~Producer();voidproduce(WareHousewh,MessageQueuemq);voidproduce(WareHousewh,MessageQueuemq,intval);private:int_id;};#endifproducer.cpp文件#includeproducer.hProducer::Producer():Producer(-1){}Producer::Producer(inti):_id(i){}Producer::~Producer(){}voidProducer::produce(WareHousewh,MessageQueuemq){produce(wh,mq,-1);}voidProducer::produce(WareHousewh,MessageQueuemq,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#includewarehouse.h#includemessagequeue.hclassConsumer{public:Consumer();Consumer(intid);~Consumer();voidconsume(WareHousewh,MessageQueuemq);private:int_id;};#endifconsumer.cpp文件umer::Consumer():Consumer(-1){}Consumer::Consumer(intid):_id(id){}Consumer::~Consumer(){}voidConsumer::consume(WareHousewh,MessageQueuemq){intvalwh.pop();stringmsg({consumer std::to_string(_id)} consume a std::to_string(val));mq.push_message(msg);}main.cpp文件#includeconsumer.h#includemessagequeue.h#includeproducer.h#includewarehouse.h#includethread#includechronousingstd::thread;usingstd::ref;usingstd::this_thread::sleep_for;voidproduce(intid,MessageQueuemq,WareHousewh){Producerp(id);intnid;while(1){p.produce(wh,mq,n);sleep_for(std::chrono::microseconds(600));}}voidconsume(intid,MessageQueuemq,WareHousewh){Consumerc(id);while(1){c.consume(wh,mq);sleep_for(std::chrono::microseconds(400));}}voidprint(MessageQueuemq){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;}运行效果因为我们没有设置终止条件所以各个线程会一直运行下去所以运行之后快速按ctrlc终止观察即可。另外需要说一点的是要在各个线程运行一次之后主动放弃运行不然就会出现一个线程长时间霸占CPU的现象这样显然是不合理的。那么至此我们就实现了生产者-消费者模型。