前言:生产者消费者模式在《大话设计模式》一书中并没有涉及。可奈何在工作中很多地方都要用到。此模式的例子基本都相似,在网上找了一篇博客(原文),在此进行剖析,写此篇博文学习分享之。
本篇博文给出详细解释,相信小白都能看得懂。
储备知识:C++多线程编程、互斥锁、状态变量。
先给出原码:
#include <cstdlib>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
static const int kItemRepositorySize = 3; // 队列大小
static const int kItemsToProduce = 10; // 待生产的数量
std::mutex mutex;//见注1。
struct ItemRepository
{
int item_buffer[kItemRepositorySize]; // 见注2
size_t read_position; // 消费者读取产品位置.见注3
size_t write_position; // 生产者写入产品位置.
std::mutex mtx; // 见注4
std::condition_variable repo_not_full; // 条件变量, 指示产品缓冲区不为满.见注5
std::condition_variable repo_not_empty; // 条件变量, 指示产品缓冲区不为空.
} gItemRepository; // 产品库全局变量, 生产者和消费者操作该变量.
typedef struct ItemRepository ItemRepository;
void ProduceItem(ItemRepository * ir, int item)
{
std::unique_lock<std::mutex> lock(ir->mtx);
while (((ir->write_position + 1) % kItemRepositorySize)== ir->read_position)//见注6.
{
{
std::lock_guard<std::mutex> lock(mutex);//注7
std::cout << "缓冲区满,等待缓冲区不满\n";
}
(ir->repo_not_full).wait(lock); // 注8
}
(ir->item_buffer)[ir->write_position] = item; // 写入产品.
(ir->write_position)++; // 写入位置后移.
if (ir->write_position == kItemRepositorySize) // 写入位置若是在队列最后则重新设置为初始位置.
ir->write_position = 0;
(ir->repo_not_empty).notify_all(); // 通知消费者产品库不为空.
lock.unlock(); // 解锁.
}
int ConsumeItem(ItemRepository *ir)
{
int data;
std::unique_lock<std::mutex> lock(ir->mtx);
while (ir->write_position == ir->read_position)
{
{
std::lock_guard<std::mutex> lock(mutex);
std::cout << "缓冲区空,等待生产者生成产品\n";
}
(ir->repo_not_empty).wait(lock); // 消费者等待"产品库缓冲区不为空"这一条件发生.
}
data = (ir->item_buffer)[ir->read_position]; // 读取某一产品
(ir->read_position)++; // 读取位置后移
if (ir->read_position >= kItemRepositorySize) // 读取位置若移到最后,则重新置位.
ir->read_position = 0;
(ir->repo_not_full).notify_all(); // 通知消费者产品库不为满.
lock.unlock(); // 解锁.
return data; // 返回产品.
}
void ProducerTask() // 生产者任务
{
for (int i = 1; i <= kItemsToProduce; ++i)
{
ProduceItem(&gItemRepository, i); // 循环生产 kItemsToProduce 个产品.
{
std::lock_guard<std::mutex> lock(mutex);
std::cout << "生产第 " << i << "个产品" << std::endl;
}
}
}
void ConsumerTask() // 消费者任务
{
static int cnt = 0;
while (1)
{
std::this_thread::sleep_for(std::chrono::seconds(1));//注9
int item = ConsumeItem(&gItemRepository); // 消费一个产品.
{
std::lock_guard<std::mutex> lock(mutex);
std::cout << "消费第" << item << "个产品" << std::endl;
}
if (++cnt == kItemsToProduce) break; // 如果产品消费个数为 kItemsToProduce, 则退出.
}
}
void InitItemRepository(ItemRepository *ir)
{
ir->write_position = 0; // 初始化产品写入位置.
ir->read_position = 0; // 初始化产品读取位置.
}
void main()
{
InitItemRepository(&gItemRepository);
std::thread producer(ProducerTask); // 创建生产者线程.
std::thread consumer(ConsumerTask); // 创建消费之线程.
producer.join();
consumer.join();
}
几点解释,大佬请绕行:
注1:此锁是多线程输出同步锁,你会发现在所有的cout上面都有一行加锁的语句std::lock_guard<std::mutex>lock(mutex);
这里的锁是为了防止多个线程向屏幕输出的时候会出现打印错乱交叉的情况。加上此锁,一个线程输出的同时,另一个线程就在等着,这样有效的避免了输出混乱。
注2:这里的整型数组是一个缓冲区,这是生产者消费者的一个重大核心。缓冲区是生产者和消费者数据交互的渠道。生产者生产的数据存入此数组中,然后消费者从此数组中取出数据。
注3:此处可以理解为一个标志位,生产者生产的数据存放到数组中,这不是一个实体可以放进去拿出来,所以弄一个标志位。
注4:此处的锁是操作整型数组缓冲区的锁,和上面输出锁一样,防止多线程操作时候,缓冲区发生混乱。
注5:此处状态变量也算是一个生产者消费者的核心内容之一。生产者生产的数据存满了缓冲区,要进行通知消费者进行消费。
注6:此处是生产是否为满的判断条件,当生产者生产一个数据write_position加1,当write_position为2的时候,说明生产了0、1、2总共三个数据,若缓冲区是3,则(2+1)%3为0,则判断为生产满的状态。
注7:此处注释和注1相呼应,但是有没有发现这里多了一个大括号{},这个大括号是加锁的括号,说明加锁的作用域只在此大括号里面。
注8:当注6的判断条件成立之后,则说明缓冲区满了,然后在这里等待,如果没有收到消费者线程的通知消息,此线程就一直在等待。
注9:这里使用了线程中的延时函数,让消费者线程延时1秒。其目的是让生产者线程在线程起来之后有足够的时间生产数据,因为此函数缓冲区设置为3,所以在及短的时间里缓冲区就满了,而此时消费者再去消费,此时的逻辑是缓冲区永远是差一个数据就发生溢出,然后生产一个数据,缓冲区溢出,通知消费者消费,然后缓冲区不为满,再生产,再溢出,再消费。