生产者消费者模型实现(基于C++)

本文详细介绍了如何通过C++编程实现生产者-消费者模型,构建了一个支持多消费者并发访问的环形缓冲区,确保了资源的有效管理和同步。关键部分包括互斥量、条件变量的使用和多线程间的协调。

通过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)

2. 

 线程同步 | 爱编程的大丙 (subingwen.cn)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值