[Linux]——基于阻塞队列的生产消费者模型

生产者消费者模型

谈生产者消费者模型之前我们必须知道什么是生产者消费者模型,看题目你可能觉得他是一个十分高深的东西,但是或许我们身边就存在生产者消费者模型。

概念引入

打个比方,生活中我们经常在缺少生活用品或者其他商品的时候我们通常会选择去超市,那也就不难理解,我们其实就是所谓的消费者。那生产者是超市么?答案是否定的,如果超市是生产者,那供货商是什么,所以供货商是所谓的生产者。再谈超市,其实超市是为了生产者和消费者交易而诞生的,所以超市就是交易场所

将场景引入到我们的软件开发中来,不难类比,一个生产数据的模块就是生产者,处理这些数据的模块可以认为是消费者,生产者生产的数据不直接交付给消费者,而是放入某个缓冲区,让消费者自己来取,那么这个缓冲区其实就可以认为是交易场所
综上,一个生产者消费者模型右三部分构成,生产资,消费者,交易场所。他们之间的关系就如同下图。
在这里插入图片描述

为什么需要它?

相信身为程序员的你每天都会做一件事,而且日复一日,那就是使用一个函数来调用另外一个函数。如下图,函数A将数据交给函数B处理,希望能的到结果返回,这里非常类型生产者和消费者,但是唯一缺少的就是我们上面所说的交易场所。正是缺少了这个交易场所,导致函数调用时出现一系列的问题,你经常发现只要函数B一旦修改就导致函数A中的调用也要修改,并且每当出现函数B奔溃时函数A也瞬间挂掉。这其实并不是最大的问题,如图,每当你调用函数B时,函数A就阻塞等待函数B,整个流程就变成了绝对的串行,函数B未执行完,函数A也将永远卡死在调用处。
在这里插入图片描述
那么解决方案其实就是需要把他们转化为一个生产消费者模型,为俩个函数之间也加上一个交易场所,这个交易场所可以存放函数A需要让函数B处理的数据,当交易场所存在数据时函数B就进行处理,否则函数A向交易场所添加数据,这个交易场所其实就是一段缓冲区,可以是链表,数组,队列任何结构,这样函数A和函数B的执行互不影响,并且大大增加了效率。
在这里插入图片描述
回到软件开发的本身归根结底来说,生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

我们生产者消费者模型使用最多的场景还是用于多线程中,在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。

生产者消费者模型的优点

  • 解耦:上面函数调用的例子其实就是为我们阐述传统函数调用强耦合的缺点,当有了此模型后购物例子中商家就不需要必须把商品送到消费者的手上,而是直接塞到超市中,这样厂家和消费者耦合度就大大减少了
  • 支持并发:如果没有超市那个交易场所,每一次购物你都需要傻傻等待商家将商品送到你手上,或者是商家每家每户询问谁家需要商品,从线程的角度来说,他们是两个独立的并发主体,互不干扰的运行。
  • 支持忙闲不均:支持忙闲不均,如果制造数据的速度时快时慢,缓冲区可以对其进行适当缓冲。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。

角色间的关系

我们之前的博客讲了Linux下的同步与互斥,所以这里生产消费者模型也满足其中的关系。很明显生产者与生产者之间是互斥关系,因为他们之间存在一种竞争,他们都想自己生产物品供被人消费。而消费者与消费者也是互斥关系,很多同学可能会觉得这是不是出错了,消费者之间怎么能是互斥关系呢,那举个例子,超市现在只剩最后一包方便面了,然而你们都饿了,此时你们简直是不是需要竞争呢?最后生产者和消费者比较独特,他们既要互斥,还要同步,因为某一个时刻他们如果同时想生产或者消费就会产生互斥现象,而且从上述例子中我们可以得出,交易场所其实是共享资源,共享资源必须合理的让所有人都能访问,这也就是为什么需要同步。

这里为了便于记忆,你可以将上面这段话总结为123原则每个生产消费者模型有1一个交易场所,2个角色,3种关系。相信现在你对生产消费者模型有了新的理解,那么我们接下来就来实现一个生产消费者模型。

基于阻塞队列的生产消费者模型

基于阻塞队列的生产者消费者模型显而易见,他最大的特点就在于他的交易场所使用了阻塞队列,对于阻塞队列你可能觉得陌生,不过你还记得Linux下进程间通信的匿名管道么?哎呦,是不是有感觉了,进程1就是生产者,进程2就是消费者,匿名管道实质上是一个文件,也是一个缓冲区,同样是我们的交易场所。我们这里实现的模型真的像极了进程间通信的管道。
在这里插入图片描述
这里我们实现了一个非常简单的阻塞队列,这里说明几点问题,因为选择了阻塞队列作为了交易场所,所以我们就需要队列的容器,但是原生的实现一个容器并不是一件容易的事,所以我们没有使用c语言,而是选择了c++,这样有的小伙伴看代码可能有的地方看不懂,不过笔者尽量写的非常偏c语言了,应该没什么大问题。

这里实现阻塞队列时为了简单起见,我们只考虑交易空的情况,当交易场所为空时通知生产者生产,所以我们就只需要一个条件变量即可,如果你想实现为空通知生产者生产,为满通知消费者消费那么你就需要再另外的增加一个条件变量来控制。

以下的代码因为没有规定队列的上限,所以为了避免生产者太快的情况我们让生产者生产一次就睡眠一秒,所以最后模型运行起来以后的现象是生产者生产一个数据,消费者就拿走一个数据。追加说明一点,因为这里的队列其实是共享资源,所以我们需要使用一把锁将他保护起来。

//queue.cc
#include<iostream>
#include<queue>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>

using namespace std;
class BlockQueue{
    private:
        queue<int> q;
        pthread_cond_t cond;
        pthread_mutex_t lock;
    private:
        void LockQueue()
        {
            pthread_mutex_lock(&lock);
        }
        void UnLockQueue()
        {
            pthread_mutex_unlock(&lock);
        }
        bool isEmpty()
        {
            return q.size() == 0 ? true : false;
        }
        void ThreadWait()
        {
            pthread_cond_wait(&cond,&lock);
        }
        void WakeOneThread()
        {
            pthread_cond_signal(&cond);
        }
    public:
        BlockQueue()
        {
            pthread_mutex_init(&lock,NULL);
            pthread_cond_init(&cond,NULL);
        }
        void PushData(const int& data)
        {
            LockQueue();
            q.push(data);
            UnLockQueue();
            cout << "product run done, data push sucess " << data <<endl;
            WakeOneThread();
        }
        void PopData(int& data)
        {
            LockQueue();
            while(isEmpty()){//使用while循环防止条件变量等待失败或者误唤醒
                ThreadWait();
            }
            data = q.front();
            q.pop();
            UnLockQueue();
            cout << "consume run done, data pop sucess : " << data <<endl;
        }
        ~BlockQueue()
        {
            pthread_cond_destroy(&cond);
            pthread_mutex_destroy(&lock);
        }
};

void* product(void* arg)//生产者生产
{
    BlockQueue* bq = (BlockQueue*)arg;
    srand((unsigned long)time(NULL));
    while(1)
    {
        int data = rand()%100 + 1;
        bq->PushData(data);
        sleep(1);//防止生产太快,每生产一条睡眠一秒
    }
}
void* consume(void* arg)//消费者消费
{
    BlockQueue* bq = (BlockQueue*)arg;
    while(1)
    {
        int d;
        bq->PopData(d);
    }
}
int main()
{
    BlockQueue bq;
    pthread_t p,c;
    pthread_create(&p,NULL,product,(void*)&bq);
    pthread_create(&c,NULL,consume,(void*)&bq);

    pthread_join(p,NULL);
    pthread_join(c,NULL);
}

代码执行结果:
在这里插入图片描述
仔细阅读上面的代码你一定能看的懂,如果有兴趣的同学还可以进一步修改代码,因为这里我们也并没有维护生产者生产者,消费者消费者之间的关系,但是想维护这俩个关系也很简单,你只需多创建几个线程,然后再创建俩把锁,再这些生产者生产者和消费者消费者竞争队列资源时,加上锁就可以。笔者这里仅为大家说明概念,篇幅太长就不在实现。

ps:不知道有没有小伙伴觉得从Linux下修改拷贝代码很麻烦,笔者偷偷告诉你个工具叫做gedit,这也是一个编辑器,你如果想拷贝很长的代码出来你用gedit + 代码文件打开文件,打开后直接用鼠标拷贝粘贴就好。昨天才无意中知道,真***的好用哈哈哈。
在这里插入图片描述
在这里插入图片描述

总结

到这里我们基于阻塞队列的生产消费者模型就介绍完了,但是小伙伴们不需要太关心代码的实现,你应该深刻理解这种模型的好处用途和思想,你要你记清楚笔者总结的那个123原则,相信能加深你对生产者消费者模型的记忆和理解,下篇博客笔者带领大家实现另一种更有意思的生产消费者模型。

### Linux 环境下信号量队的使用与实现 #### 1. 信号量的基本概念 信号量是一种用于解决多线程或多进程之间同步和互斥问题的重要工具。在 Linux 中,信号量分为两种主要类型:**互斥信号量(mutex semaphore)** 和 **计数信号量(counting semaphore)**[^1]。 - **互斥信号量**主要用于保护临界区,确保某一时刻只有一个线程或进程能够访问特定资源。 - **计数信号量**则允许多个线程或进程按一定条件竞争资源,其核心是一个整数值,表示可用资源的数量。 #### 2. 信号量队的作用 当某个进程尝试获取一个已经被占用的信号量时,该进程会被挂起并加入到信号量对应的等待队中。一旦持有信号量的进程释放了信号量,内核会从等待队中唤醒下一个合适的进程继续运行。 这种机制的核心在于维护了一个有序的等待表,称为**信号量队**。通过这种方式,Linux 实现了高效的任务调度和资源共享管理。 #### 3. 信号量队的具体实现 以下是基于 Linux 的信号量队的主要实现细节: ##### (1)数据结构定义 Linux 内核中使用的信号量由 `struct semaphore` 定义,其中包含了重要的字段来描述信号量的状态和关联的等待队: ```c struct semaphore { spinlock_t lock; unsigned int count; /* 计数器 */ struct list_head wait_list; /* 等待队头节点 */ }; ``` - `count`: 表示当前可用资源数量。对于互斥信号量而言,初始值通常为 1;而对于计数信号量,则可以根据实际需求设置不同的初值。 - `wait_list`: 是一个双向链表,存储所有因无法立即获得信号量而进入睡眠状态的任务。 ##### (2)初始化过程 创建一个新的信号量对象之前需要对其进行初始化操作。可以通过调用函数 `sema_init()` 来完成这一工作: ```c void sema_init(struct semaphore *sem, int val); ``` 参数说明如下: - `sem`: 待初始化的目标信号量指针; - `val`: 初始计数值。 例如,下面代码片段展示了如何声明并初始化一个互斥锁类型的信号量: ```c #include <linux/semaphore.h> struct semaphore my_mutex; // 初始化互斥信号量 sema_init(&my_mutex, 1); // 设置初始值为1 ``` ##### (3)获取信号量 要请求占有某信号量,可利用宏 `down()` 或更安全版本 `down_interruptible()` 函数: ```c int down_interruptible(struct semaphore *sem); ``` 如果目标信号量不可用(即 `count == 0`),调用方将被阻塞直到满足条件为止。返回值指示是否成功取得锁定权柄——零代表正常结束,负错误码意味着中断发生或其他异常情况。 对应地,在退出临界区域之后应当及时释放所持信号量使用权,这可通过上调方法达成目的: ```c void up(struct semaphore *sem); ``` 上述动作实际上增加了内部计数变量,并激活可能存在的首个候补者使其恢复活动态。 #### 4. 应用场景举例 假设存在一组生产者消费者模型的应用场合,它们共同作用于同一个缓冲池之上。为了避免冲突现象的发生,我们可以引入一对相互配合工作的二元型信号量分别监控空闲槽位数目以及已填充项目总量变化趋势[^4]: ```c struct semaphore empty_slots; // 控制剩余空间大小 struct semaphore filled_items; // 追踪现存有效条目统计 /* 生产者的逻辑部分摘录 */ produce_item(item) { down(&empty_slots); // 尝试减少空白位置指标 insert_into_buffer(item); // 插入新产生的实体至共享容器里边去 up(&filled_items); // 增加已完成制品计量单位 } /* 消费者的处理流程示意 */ consume_item() { item result; down(&filled_items); // 验证是否有东西可供提取出来 remove_from_buffer(result);// 取走指定成员项实例化局部副本 up(&empty_slots); // 提供额外容量给后续制造环节使用 process_result(result); // 对所得成果进一步加工处置 } ``` 在此范例当中,每当有一件商品生成完毕后都会相应增加填满物事标记的同时削减闲置场所额度;反之亦然,取走一件成品也会重新腾出一处容纳地点机会供给上游工序循环再利用之需。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值