通过C++实现并发执行的生产者消费者问题
一、实验内容
创建一个一对多的生产者-消费者模型,可以允许多个消费者同时从产品库中获取产品,所以需要记录各个消费者取走产品的总的个数,不能出现产品库中没有产品但是消费者还可以访问的情况,否则会发生错误。
在此使用代码实现一个生产者对应三个消费者情况。缓冲区设为5,计划生产产品数量为15。
二、背景知识
生产者与消费者模型是一个著名的同步问题,它是基于等待/通知机制实现的。有一或多块缓冲区作为公共区域,生产者生产完产品放入该区域,消费者消费是从区域中拿走产品。它比较注意的是要实现以下几点:
1.生产者生产时,消费者不能消费,反之同理,二者一直为互斥状态。
2.当缓冲区为空时,消费者不能消费。
3.当缓冲区满时,生产者不能生产。
第2和第3点说明了生产者和消费者对缓冲区的访问必须是同步的,否则缓冲区的建立就没有意义。
三、核心代码
1.结构体定义
将产品库的各种变量集成到结构体中
struct resource {
int buf[bufSize]; // 产品缓冲区, 配合 read_pos 和 write_pos 模型环形队列
size_t read_pos; // 消费者读取产品位置
size_t write_pos; // 生产者写入产品位置(size_t:无符号的整型数,它的取值没有负数,在数组中也用不到负数,而它的取值范围是整型数的双倍)
size_t item_counter; //计数器
std::mutex mtx; // 互斥量,保护产品缓冲区
std::mutex item_counter_mtx;
std::condition_variable not_full; // 条件变量, 指示产品缓冲区不为满
std::condition_variable not_empty; // 条件变量, 指示产品缓冲区不为空
} instance; // 产品库全局变量, 生产者和消费者操作该变量
2.指针的初始化
初始化在产品库中的指针
void Initresource(resource *ir)
{
ir->write_pos = 0; // 初始化产品写入位置
ir->read_pos = 0; // 初始化产品读取位置
ir->item_counter = 0;
}
3. 生产者进程函数定义
本函数将生产出来的产品放入产品库中(移动指针),最后释放lock这个互斥变量
void Producer(resource *ir, int item) //定义资源指针和项目大小
{
std::unique_lock<std::mutex> lock(ir->mtx);
while (((ir->write_pos + 1) % bufSize) == ir->read_pos) { // 项目缓冲区已满,需要等待
std::cout << "生产者正在等待位置...\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(); // 解锁
}
4.生产者任务
输出产品生产信息,循环Producer函数
void ProducerTask()
{
for (int i = 1; i <= ProNum; ++i) {
// sleep(1);
std::cout << "生产者进程 " << std::this_thread::get_id() //返回一个独一无二的线程号
<< " 产生第 " << i << " 项..." << std::endl;
Producer(&instance, i); // 循环生产 ProNum 个产品
}
std::cout << "生产者进程 " << std::this_thread::get_id()
<< " 正在退出..." << std::endl;
}
5.消费者进程函数定义
将产品库中的产品取出并移动指针,判断指针位置最后释放lock互斥变量,最后将取出的产品返回。
int Consumer(resource *ir)
{
int data;
std::unique_lock<std::mutex> lock(ir->mtx);
// 项目缓冲区为空,请在此处等待
while (ir->write_pos == ir->read_pos) {
std::cout << "消费者正在等待物品...\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; // 返回产品
}
6.消费者任务
输出消费者正在消费的产品信息,需要先判断输出产品数量是否小于产品总数ProNum,如果小于才能将该产品输出,否则就令exit为true,直接退出消费者进程
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 << "消费者进程 " << std::this_thread::get_id() // 同上
<< " 正在消费第 " << item << " 项" << std::endl;
}
else
ready_to_exit = true;
if (ready_to_exit == true)
break;
}
std::cout << "消费者进程 " << std::this_thread::get_id() // 同上
<< "正在退出 " << std::endl;
}
7.主函数
先定义一个产品库,将生产者生产进程输出,然后开始消费者访问产品库,最后当所有子线程全部完成后,主线程才能继续执行,保证了当消费者结束进程后生产者能继续执行到产品数为PorNum为止,而不会发生资源泄漏。
int main()
{
Initresource(&instance);
std::thread producer(ProducerTask); //join()操作是在std::thread t(func)后“某个”合适的地方调用,其作用是回收对应创建的线程的资源,避免造成资源的泄露。
std::thread consumer1(ConsumerTask);
std::thread consumer2(ConsumerTask);
std::thread consumer3(ConsumerTask);producer.join();
consumer1.join();
consumer2.join();
consumer3.join();
}
四、运行情况

五、结论以及实验中所产生的问题
1.每个函数中至少都有三个判断,需要写代码时仔细思考各个条件是否符合要求。
2.设置结构体时漏了互斥量以及条件变量的定义,查阅资料后加上了互斥量以及条件变量,这两个一个是判断能否访问生产进程或消费进程这两个互斥进程,一个是判断队满、不满或空的条件,至关重要。
3.指针的指向必须明确,要考虑清楚在写指针的移动代码,否则很容易将自己绕晕。
4.在写代码时不知道如何区分各个进程,通过查阅资料知道了以下函数,可以返回一个独一无二的线程号,将此代码写入cout语句中即可。
std::this_thread::get_id() //返回一个独一无二的线程号
5.lock函数可以直接代替wait函数使用,解锁和锁定都可以通过它。
6.通过创建一个队列储存生产的数据,然后判断队列是否空或满,以此来判断可否生产和消费,为此,需要创建一个产品数量上限值ProNum来进行比较。
7.各个函数定义(生产队列、消费队列、生产任务、消费任务)其中的细节:①不管在哪个函数中,首先判断是否有空的资源缓冲区,判断有空的资源缓冲区后才可进行下面的操作。②循环判断目前队列情况,对特殊队列要进行特殊的处理(例如满和空的队列)。③进行读取或写入操作后进行资源指针的移动,移动完判断此时指针的位置,如果队列不满或不空,则解锁。
参考
1.
C++11 并发指南九(综合运用: C++11 多线程下生产者消费者模型详解) - Haippy - 博客园 (cnblogs.com)
本文详细介绍了如何通过C++编程实现生产者-消费者模型,构建了一个支持多消费者并发访问的环形缓冲区,确保了资源的有效管理和同步。关键部分包括互斥量、条件变量的使用和多线程间的协调。
1万+

被折叠的 条评论
为什么被折叠?



