生产者和消费者模型
一、什么是生产者-消费者模型
1、简单理解生产者-消费者模型
假设有两个进程(或线程)A、B和一个固定大小的缓冲区,A进程生产数据放入缓冲区,B进程从缓冲区中取出数据进行计算,这就是一个简单的生产者-消费者模型。这里的A进程相当于生产者,B进程相当于消费者。
2、为什么要使用生产者-消费者模型
在多线程开发中,如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完数据才能够继续生产数据,因为生产过多的数据可能会导致存储不足;同理如果消费者的速度大于生产者那么消费者就会经常处理等待状态,所以为了达到生产者和消费者生产数据和消费数据之间的平衡,那么就需要一个缓冲区用来存储生产者生产的数据,所以就引入了生产者-消费者模式
简单来说,这里缓冲区的作用就是为了平衡生产者和消费者的数据处理能力,一方面起到缓存作用,另一方面达到解耦合作用。
3、生产者-消费者模型特点
- 保证生产者不会在缓冲区满的时候继续向缓冲区放入数据,而消费者也不会在缓冲区空的时候,消耗数据
- 当缓冲区满的时候,生产者会进入休眠状态,当下次消费者开始消耗缓冲区的数据时,生产者才会被唤醒,开始往缓冲区中添加数据;当缓冲区空的时候,消费者也会进入休眠状态,直到生产者往缓冲区中添加数据时才会被唤醒
4、生产者-消费者模型的应用场景
生产者-消费者模型一般用于将生产数据的一方和消费数据的一方分割开来,将生产数据与消费数据的过程解耦开来。
1)Excutor任务执行框架:
-
通过将任务的提交和任务的执行解耦开来,提交任务的操作相当于生产者,执行任务的操作相当于消费者;例如使用Excutor构建web服务器,用于处理线程的请求:生产者将任务提交给线程池,线程池创建线程处理任务,如果需要运行的任务数大于线程池的基本线程数,那么就把任务扔到阻塞队列(通过线程池 + 阻塞队列的方式比只使用一个阻塞队列的效率高很多,因为消费者能够处理就直接处理掉了,不用每个消费者都要先从阻塞队列中取出任务再执行)
2)消息中间件active MQ:
-
双十一的时候,会产生大量的订单,那么不可能同时处理那么多的订单,需要将订单放入一个队列里面,然后由专门的线程处理订单。这里用户下单就是生产者,处理订单的线程就是消费者;再比如12306的抢票功能,先由一个容器存储用户提交的订单,然后再由专门处理订单的线程慢慢处理,这样可以在短时间内支持高并发服务
3)任务的处理时间比较长的情况下:
-
比如上传附近并处理,那么这个时候可以将用户上传和处理附件分成两个过程,用一个队列暂时存储用户上传的附近,然后立刻返回用户上传成功,然后有专门的线程处理队列中的附近
5、生产者-消费者模型的优点
1)解耦合:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理。
2)复用:通过将生产者类和消费者类独立开来,那么可以对生产者类和消费者类进行独立的复用与扩展。
3)调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,给予慢的一方多的并发数,来提高任务的处理速度。
4)异步:对于生产者和消费者来说能够各司其职,生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;同样的对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,通过异步的方式支持高并发,将一个耗时的流程拆成生产和消费两个阶段,这样生产者因为执行 put() 的时间比较短,而支持高并发。
5)支持分布式:生产者和消费者通过队列进行通讯,所以不需要运行在同一台机器上,在分布式环境中可以通过 redis 的 list 作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉。
二、C++实现生产者-消费者模型
1、实现细节
- 具体的实现逻辑是构建一个queue来存储生产的数据,queue不满时可以生产,不空时可以消费。
- 对于这个队列,采用阻塞队列的实现思路。
- 先实现构造函数,初始化一个unique_lock供condition_variable使用。
- 如何在类里面使用unique_lock等需要初始化,并且初始化会加锁的对象。这要研究下。我的理解是构造列表初始化,然后函数体里unlock。
- 对于条件变量,申请两个,分别控制consumer和producer。
- 然后就是入和出队列的细节。
- 首先加锁。
- 循环判断一下目前的队列情况,对于各自的特殊情况(队满和队空)进行处理。
- 唤醒一个线程来处理特殊情况。
- 等待处理完毕。
- 处理入和出队列操作。
- 最后释放锁。
2、单生产者-单消费者模型
- 单生产者-单消费者模型中只有一个生产者和一个消费者,
- 生产者不停地往产品库中放入产品,
- 消费者则从产品库中取走产品,
- 产品库容积有限制,只能容纳一定数目的产品,
- 如果生产者生产产品的速度过快,则需要等待消费者取走产品之后,产品库不为空才能继续往产品库中放置新的产品,
- 相反,如果消费者取走产品的速度过快,则可能面临产品库中没有产品可使用的情况,此时需要等待生产者放入一个产品后,消费者才能继续工作。
C++11实现单生产者单消费者模型的代码如下:
#include <unistd.h> #include <cstdlib> #include <condition_variable> #include <iostream> #include <mutex> #include <thread> static const int bufSize = 10; // Item buffer size. static const int ProNum = 20; // How many items we plan to produce. struct resource { int buf[bufSize]; // 产品缓冲区, 配合 read_pos 和 write_pos 模型环形队列. size_t read_pos; // 消费者读取产品位置. size_t write_pos; // 生产者写入产品位置. std::mutex mtx; // 互斥量,保护产品缓冲区 std::condition_variable not_full; // 条件变量, 指示产品缓冲区不为满. std::condition_variable not_empty; // 条件变量, 指示产品缓冲区不为空. } instance; // 产品库全局变量, 生产者和消费者操作该变量. typedef struct resource resource; void Producer(resource *ir, int item) { std::unique_lock<std::mutex> lock(ir->mtx); while (((ir->write_pos + 1) % bufSize) == ir->read_pos) { // item buffer is full, just wait here. std::cout << "Producer is waiting for an empty slot...\n"; (ir->not_full).wait(lock); // 生产者等待"产品库缓冲区不为满"这一条件发生. } (ir->buf)[ir->write_pos] = item; // 写入产品. (ir->write_pos)++; // 写入位置后移. if (ir->write_pos == bufSize) // 写入位置若是在队列最后则重新设置为初始位置. ir->write_pos = 0; (ir->not_empty).notify_all(); // 通知消费者产品库不为空. } int Consumer(resource *ir) { int data; std::unique_lock<std::mutex> lock(ir->mtx); // item buffer is empty, just wait here. while (ir->write_pos == ir->read_pos) { std::cout << "Consumer is waiting for items...\n"; (ir->not_empty).wait(lock); // 消费者等待"产品库缓冲区不为空"这一条件发生. } data = (ir->buf)[ir->read_pos]; // 读取某一产品 (ir->read_pos)++; // 读取位置后移 if (ir->read_pos >= bufSize) // 读取位置若移到最后,则重新置位. ir->read_pos = 0; (ir->not_full).notify_all(); // 通知消费者产品库不为满. return data; // 返回产品. } void ProducerTask() // 生产者任务 { for (int i = 1; i <= ProNum; ++i) { // sleep(1); std::cout << "Produce the " << i << "^th item..." << std::endl; Producer(&instance, i); // 循环生产 ProNum 个产品. } } void ConsumerTask() // 消费者任务 { static int cnt = 0; while (1) { sleep(1); int item = Consumer(&instance); // 消费一个产品. std::cout << "Consume the " << item << "^th item" << std::endl; if (++cnt == ProNum) break; // 如果产品消费个数为 ProNum, 则退出. } } void Initresource(resource *ir) { ir->write_pos = 0; // 初始化产品写入位置. ir->read_pos = 0; // 初始化产品读取位置. } int main() { Initresource(&instance); std::thread producer(ProducerTask); // 创建生产者线程. std::thread consumer(ConsumerTask); // 创建消费之线程. producer.join(); consumer.join(); }
3、单生产者-多消费者模型
与单生产者和单消费者模型不同的是,单生产者-多消费者模型中可以允许多个消费者同时从产品库中取走产品。所以除了保护产品库在多个读写线程下互斥之外,还需要维护消费者取走产品的计数器,代码如下:
#include <unistd.h> #include <cstdlib> #include <condition_variable> #include <iostream> #include <mutex> #include <thread> static const int bufSize = 8; // Item buffer size. static const int ProNum = 30; // How many items we plan to produce. struct resource { int buf[bufSize]; // 产品缓冲区, 配合 read_pos 和 write_pos 模型环形队列. size_t read_pos; // 消费者读取产品位置. size_t write_pos; // 生产者写入产品位置. size_t item_counter; std::mutex mtx; // 互斥量,保护产品缓冲区 std::mutex item_counter_mtx; std::condition_variable not_full; // 条件变量, 指示产品缓冲区不为满. std::condition_variable not_empty; // 条件变量, 指示产品缓冲区不为空. } instance; // 产品库全局变量, 生产者和消费者操作该变量. typedef struct resource resource; void Producer(resource *ir, int item) { std::unique_lock<std::mutex> lock(ir->mtx); while (((ir->write_pos + 1) % bufSize) == ir->read_pos) { // item buffer is full, just wait here. std::cout << "Producer is waiting for an empty slot...\n"; (ir->not_full).wait(lock); // 生产者等待"产品库缓冲区不为满"这一条件发生. } (ir->buf)[ir->write_pos] = item; // 写入产品. (ir->write_pos)++; // 写入位置后移. if (ir->write_pos == bufSize) // 写入位置若是在队列最后则重新设置为初始位置. ir->write_pos = 0; (ir->not_empty).notify_all(); // 通知消费者产品库不为空. lock.unlock(); // 解锁. } int Consumer(resource *ir) { int data; std::unique_lock<std::mutex> lock(ir->mtx); // item buffer is empty, just wait here. while (ir->write_pos == ir->read_pos) { std::cout << "Consumer is waiting for items...\n"; (ir->not_empty).wait(lock); // 消费者等待"产品库缓冲区不为空"这一条件发生. } data = (ir->buf)[ir->read_pos]; // 读取某一产品 (ir->read_pos)++; // 读取位置后移 if (ir->read_pos >= bufSize) // 读取位置若移到最后,则重新置位. ir->read_pos = 0; (ir->not_full).notify_all(); // 通知消费者产品库不为满. lock.unlock(); // 解锁. return data; // 返回产品. } void ProducerTask() // 生产者任务 { for (int i = 1; i <= ProNum; ++i) { // sleep(1); std::cout << "Producer thread " << std::this_thread::get_id() << " producing the " << i << "^th item..." << std::endl; Producer(&instance, i); // 循环生产 ProNum 个产品. } std::cout << "Producer thread " << std::this_thread::get_id() << " is exiting..." << std::endl; } void ConsumerTask() // 消费者任务 { bool ready_to_exit = false; while (1) { sleep(1); std::unique_lock<std::mutex> lock(instance.item_counter_mtx); if (instance.item_counter < ProNum) { int item = Consumer(&instance); ++(instance.item_counter); std::cout << "Consumer thread " << std::this_thread::get_id() << " is consuming the " << item << "^th item" << std::endl; } else ready_to_exit = true; if (ready_to_exit == true) break; } std::cout << "Consumer thread " << std::this_thread::get_id() << " is exiting..." << std::endl; } void Initresource(resource *ir) { ir->write_pos = 0; // 初始化产品写入位置. ir->read_pos = 0; // 初始化产品读取位置. ir->item_counter = 0; } int main() { Initresource(&instance); std::thread producer(ProducerTask); std::thread consumer1(ConsumerTask); std::thread consumer2(ConsumerTask); std::thread consumer3(ConsumerTask); std::thread consumer4(ConsumerTask); producer.join(); consumer1.join(); consumer2.join(); consumer3.join(); consumer4.join(); }
4、多生产者-单消费者模型
与单生产者和单消费者模型不同的是,多生产者-单消费者模型中可以允许多个生产者同时向产品库中放入产品。所以除了保护产品库在多个读写线程下互斥之外,还需要维护生产者放入产品的计数器,代码如下:
#include <unistd.h> #include <cstdlib> #include <condition_variable> #include <iostream> #include <mutex> #include <thread> static const int bufSize = 8; // Item buffer size. static const int ProNum = 20; // How many items we plan to produce. struct resource { int buf[bufSize]; // 产品缓冲区, 配合 read_pos 和 write_pos 模型环形队列. size_t read_pos; // 消费者读取产品位置. size_t write_pos; // 生产者写入产品位置. size_t item_counter; std::mutex mtx; // 互斥量,保护产品缓冲区 std::mutex item_counter_mtx; std::condition_variable not_full; // 条件变量, 指示产品缓冲区不为满. std::condition_variable not_empty; // 条件变量, 指示产品缓冲区不为空. } instance; // 产品库全局变量, 生产者和消费者操作该变量. typedef struct resource resource; void Producer(resource *ir, int item) { std::unique_lock<std::mutex> lock(ir->mtx); while (((ir->write_pos + 1) % bufSize) == ir->read_pos) { // item buffer is full, just wait here. std::cout << "Producer is waiting for an empty slot...\n"; (ir->not_full).wait(lock); // 生产者等待"产品库缓冲区不为满"这一条件发生. } (ir->buf)[ir->write_pos] = item; // 写入产品. (ir->write_pos)++; // 写入位置后移. if (ir->write_pos == bufSize) // 写入位置若是在队列最后则重新设置为初始位置. ir->write_pos = 0; (ir->not_empty).notify_all(); // 通知消费者产品库不为空. } int Consumer(resource *ir) { int data; std::unique_lock<std::mutex> lock(ir->mtx); // item buffer is empty, just wait here. while (ir->write_pos == ir->read_pos) { std::cout << "Consumer is waiting for items...\n"; (ir->not_empty).wait(lock); // 消费者等待"产品库缓冲区不为空"这一条件发生. } data = (ir->buf)[ir->read_pos]; // 读取某一产品 (ir->read_pos)++; // 读取位置后移 if (ir->read_pos >= bufSize) // 读取位置若移到最后,则重新置位. ir->read_pos = 0; (ir->not_full).notify_all(); // 通知消费者产品库不为满. return data; // 返回产品. } void ProducerTask() // 生产者任务 { bool ready_to_exit = false; while (1) { sleep(1); std::unique_lock<std::mutex> lock(instance.item_counter_mtx); if (instance.item_counter < ProNum) { ++(instance.item_counter); Producer(&instance, instance.item_counter); std::cout << "Producer thread " << std::this_thread::get_id() << " is producing the " << instance.item_counter << "^th item" << std::endl; } else ready_to_exit = true; if (ready_to_exit == true) break; } std::cout << "Producer thread " << std::this_thread::get_id() << " is exiting..." << std::endl; } void ConsumerTask() // 消费者任务 { static int cnt = 0; while (1) { sleep(1); cnt++; if (cnt <= ProNum) { int item = Consumer(&instance); // 消费一个产品. std::cout << "Consumer thread " << std::this_thread::get_id() << " is consuming the " << item << "^th item" << std::endl; } else break; // 如果产品消费个数为 ProNum, 则退出. } std::cout << "Consumer thread " << std::this_thread::get_id() << " is exiting..." << std::endl; } void Initresource(resource *ir) { ir->write_pos = 0; // 初始化产品写入位置. ir->read_pos = 0; // 初始化产品读取位置. ir->item_counter = 0; } int main() { Initresource(&instance); std::thread producer1(ProducerTask); std::thread producer2(ProducerTask); std::thread producer3(ProducerTask); std::thread producer4(ProducerTask); std::thread consumer(ConsumerTask); producer1.join(); producer2.join(); producer3.join(); producer4.join(); consumer.join(); }
5、多生产者-多消费者模型
该模型可以说是前面两种模型的综合,程序需要维护两个计数器,分别是生产者已生产产品的数目和消费者已取走产品的数目。另外也需要保护产品库在多个生产者和多个消费者互斥地访问。
#include <unistd.h> #include <cstdlib> #include <condition_variable> #include <iostream> #include <mutex> #include <thread> static const int bufSize = 8; // Item buffer size. static const int ProNum = 20; // How many items we plan to produce. struct resource { int buf[bufSize]; // 产品缓冲区, 配合 read_pos 和 write_pos 模型环形队列. size_t read_pos; // 消费者读取产品位置. size_t write_pos; // 生产者写入产品位置. size_t pro_item_counter; size_t con_item_counter; std::mutex mtx; // 互斥量,保护产品缓冲区 std::mutex pro_mtx; std::mutex con_mtx; std::condition_variable not_full; // 条件变量, 指示产品缓冲区不为满. std::condition_variable not_empty; // 条件变量, 指示产品缓冲区不为空. } instance; // 产品库全局变量, 生产者和消费者操作该变量. typedef struct resource resource; void Producer(resource *ir, int item) { std::unique_lock<std::mutex> lock(ir->mtx); while (((ir->write_pos + 1) % bufSize) == ir->read_pos) { // item buffer is full, just wait here. std::cout << "Producer is waiting for an empty slot...\n"; (ir->not_full).wait(lock); // 生产者等待"产品库缓冲区不为满"这一条件发生. } (ir->buf)[ir->write_pos] = item; // 写入产品. (ir->write_pos)++; // 写入位置后移. if (ir->write_pos == bufSize) // 写入位置若是在队列最后则重新设置为初始位置. ir->write_pos = 0; (ir->not_empty).notify_all(); // 通知消费者产品库不为空. } int Consumer(resource *ir) { int data; std::unique_lock<std::mutex> lock(ir->mtx); // item buffer is empty, just wait here. while (ir->write_pos == ir->read_pos) { std::cout << "Consumer is waiting for items...\n"; (ir->not_empty).wait(lock); // 消费者等待"产品库缓冲区不为空"这一条件发生. } data = (ir->buf)[ir->read_pos]; // 读取某一产品 (ir->read_pos)++; // 读取位置后移 if (ir->read_pos >= bufSize) // 读取位置若移到最后,则重新置位. ir->read_pos = 0; (ir->not_full).notify_all(); // 通知消费者产品库不为满. return data; // 返回产品. } void ProducerTask() // 生产者任务 { bool ready_to_exit = false; while (1) { sleep(1); std::unique_lock<std::mutex> lock(instance.pro_mtx); if (instance.pro_item_counter < ProNum) { ++(instance.pro_item_counter); Producer(&instance, instance.pro_item_counter); std::cout << "Producer thread " << std::this_thread::get_id() << " is producing the " << instance.pro_item_counter << "^th item" << std::endl; } else ready_to_exit = true; lock.unlock(); if (ready_to_exit == true) break; } std::cout << "Producer thread " << std::this_thread::get_id() << " is exiting..." << std::endl; } void ConsumerTask() // 消费者任务 { bool ready_to_exit = false; while (1) { sleep(1); std::unique_lock<std::mutex> lock(instance.con_mtx); if (instance.con_item_counter < ProNum) { int item = Consumer(&instance); ++(instance.con_item_counter); std::cout << "Consumer thread " << std::this_thread::get_id() << " is consuming the " << item << "^th item" << std::endl; } else ready_to_exit = true; lock.unlock(); if (ready_to_exit == true) break; } std::cout << "Consumer thread " << std::this_thread::get_id() << " is exiting..." << std::endl; } void Initresource(resource *ir) { ir->write_pos = 0; // 初始化产品写入位置. ir->read_pos = 0; // 初始化产品读取位置. ir->pro_item_counter = 0; ir->con_item_counter = 0; } int main() { Initresource(&instance); std::thread producer1(ProducerTask); std::thread producer2(ProducerTask); std::thread producer3(ProducerTask); std::thread producer4(ProducerTask); std::thread consumer1(ConsumerTask); std::thread consumer2(ConsumerTask); std::thread consumer3(ConsumerTask); std::thread consumer4(ConsumerTask); producer1.join(); producer2.join(); producer3.join(); producer4.join(); consumer1.join(); consumer2.join(); consumer3.join(); consumer4.join(); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了