Linux(十三) 生产者消费者模型

什么是生产者消费者问题

比如有两个进程A和B,它们共享一个固定大小的缓冲区,A进程产生数据放入缓冲区,B进程从缓冲区中取出数据进行计算,那么这里其实就是一个生产者和消费者的模式,A相当于生产者,B相当于消费者

为什么要使用生产者消费者模式

在多线程开发中,如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完了数据才能够继续生产数据,因为生产那么多也没有地方放啊;
同理如果消费者的速度大于生产者那么消费者就会经常处于等待状态,所以为了达到生产者和消费者生产数据和消费数据之间的平衡,那么就需要一个缓冲区用来存储生产者生产的数据,所以就引入了生产者-消费者模式。
简单来说这里的缓冲区的作用就是为了平衡生产者和消费者的处理能力,起到一个数据缓存的作用,同时也达到了一个解耦的作用
在这里插入图片描述
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者-消费者模式的优点

  • 解耦:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理
  • 复用:通过将生产者类和消费者类独立开来,那么可以对生产者类和消费者类进行独立的复用与扩展
    调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,给予慢的一方多的并发数,来提高任务的处理速度
  • 异步:对于生产者和消费者来说能够各司其职,生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;同样的对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,通过异步的方式支持高并发,将一个耗时的流程拆成生产和消费两个阶段,这样生产者因为执行put()的时间比较短,而支持高并发
  • 支持分布式:生产者和消费者通过队列进行通讯,所以不需要运行在同一台机器上,在分布式环境中可以通过redis的list作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉

生产者-消费者模式的应用场景

生产者-消费者模式一般用于将生产数据的一方和消费数据的一方分割开来,将生产数据与消费数据的过程解耦开来

  • Excutor任务执行框架:

    • 通过将任务的提交和任务的执行解耦开来,提交任务的操作相当于生产者,执行任务的操作相当于消费者
    • 例如使用Excutor构建web服务器,用于处理线程的请求:生产者将任务提交给线程池,线程池创建线程处理任务,如果需要运行的任务数大于线程池的基本线程数,那么就把任务扔到阻塞队列(通过线程池+阻塞队列的方式比只使用一个阻塞队列的效率高很多,因为消费者能够处理就直接处理掉了,不用每个消费者都要先从阻塞队列中取出任务再执行)
  • 消息中间件activeMQ:

    • 双十一的时候,会产生大量的订单,那么不可能同时处理那么多的订单,需要将订单放入一个队列里面,然后由专门的线程处理订单。这里用户下单就是生产者,处理订单的线程就是消费者;再比如12306的抢票功能,先由一个容器存储用户提交的订单,然后再由专门处理订单的线程慢慢处理,这样可以在短时间内支持高并发服务
  • 任务的处理时间比较长的情况下:

    • 比如上传附近并处理,那么这个时候可以将用户上传和处理附件分成两个过程,用一个队列暂时存储用户上传的附近,然后立刻返回用户上传成功,然后有专门的线程处理队列中的附近

生产者-消费者模式的特点

  • 保证生产者不会在缓冲区满的时候继续向缓冲区放入数据,而消费者也不会在缓冲区空的时候,消耗数据
  • 当缓冲区满的时候,生产者会进入休眠状态,当下次消费者开始消耗缓冲区的数据时,生产者才会被唤醒,开始往缓冲区中添加数据;当缓冲区空的时候,消费者也会进入休眠状态,直到生产者往缓冲区中添加数据时才会被唤醒

生产者-消费者模式的实现

阻塞队列

基于BlockingQueue的生产者消费者模型 BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
首先我们从最简单的开始,假设只有一个生产者线程执行put操作,向缓冲区中添加数据,同时也只有一个消费者线程从缓冲区中取出数据
在这里插入图片描述

// BlockQueue.hpp
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include "lockGuard.hpp"
#define BLOCK_NUM 5

template<class T>
class BlockQueue
{
private:
    bool Empty()
    {
        return bq_.size() == 0;
    }
    bool Full()
    {
        return bq_.size() == capacity_;
    }
public:
    BlockQueue(int capacity = BLOCK_NUM)
        :capacity_(capacity)
    {
        pthread_mutex_init(&mtx_,nullptr);
        pthread_cond_init(&empty_,nullptr);
        pthread_cond_init(&full_,nullptr);
    }
    void push(const T& in)
    {
        // pthread_mutex_lock(&mtx_);
        // // 1.先检测当前的临界区是否能够满足访问条件
        // // pthread_cond_wait:我是在临界区中,我是持有锁的,如果我去等待了,锁该怎么办
        // // pthread_cond_wait:第二个参数是一个锁,当成功调用wait后,该锁会被自动释放
        // // 当我被唤醒的时候,我是从哪里醒来的?从哪里阻塞挂起,就从哪里醒来,我们是在临界区被唤醒的
        // // 当我们被唤醒的时候,pthread_cond_wait会自动帮我们申请锁
        // // pthread_cond_wait: 但是只要是一个函数,就可能调用失败
        // // pthread_cond_wait: 可能存在 伪唤醒 的情况
        // while(Full()) pthread_cond_wait(&full_,&mtx_);
        // // 2.访问临界资源,100%确定,资源是就绪的
        // bq_.push(in);
        // //if(bq_.size() >= capacity_/2) pthread_cond_signal(&empty_);
        // pthread_cond_signal(&empty_);
        // pthread_mutex_unlock(&mtx_);
        lockGuard lockguard(&mtx_);
        while(Full()) pthread_cond_wait(&full_,&mtx_);
        
        bq_.push(in);
        pthread_cond_signal(&empty_);
    }
    void pop(T* out)
    {
        // pthread_mutex_lock(&mtx_);
        // while(Empty()) pthread_cond_wait(&empty_,&mtx_);
        // *out = bq_.front();
        // bq_.pop();
        // pthread_cond_signal(&full_);
        // pthread_mutex_unlock(&mtx_);
        lockGuard lockguard(&mtx_);
        while(Empty()) pthread_cond_wait(&empty_,&mtx_);
        *out = bq_.front();
        bq_.pop();
        pthread_cond_signal(&full_);
    }

private:
    std::queue<T> bq_;     // 阻塞队列
    int capacity_;         // 容量上限
    pthread_mutex_t mtx_;  // 通过互斥锁来保证线程安全
    pthread_cond_t empty_; // 表示queue是否为空
    pthread_cond_t full_;  // 表示queue是否为满
};
// ConProd.cc
#include "BlockQueue.hpp"
#include "task.hpp"
#include <ctime>
int myAdd(int x, int y)
{
    return x + y;
}

void *Consumer(void *args)
{
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    while (true)
    {
        // int a = 0;
        Task t;
        bq->pop(&t);
        std::cout << pthread_self() << "Consumer:" << t.x_ << "+" << t.y_ << "=" << t() << std::endl;
        sleep(1);
    }
    return nullptr;
}
void *Product(void *args)
{
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    int a = 0;
    while (true)
    {
        int x = rand()%10 + 1;
        usleep(rand() % 1000);
        int y = rand() % 9;
        Task t(x,y,myAdd);
        bq->push(t);
        std::cout << pthread_self() << "Product:" << t.x_ << "+" << t.y_ << "= ?" << std::endl;
        a++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((unsigned int)time(NULL) ^ getpid());
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    pthread_t consumer[2], product[2];
    pthread_create(consumer, nullptr, Consumer, bq);
    pthread_create(consumer + 1, nullptr, Consumer, bq);
    pthread_create(product, nullptr, Product, bq);
    pthread_create(product + 2, nullptr, Product, bq);

    pthread_join(consumer[0], nullptr);
    pthread_join(consumer[1], nullptr);
    pthread_join(product[0], nullptr);
    pthread_join(product[1], nullptr);
    delete bq;
    return 0;
}
// lockGuard.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
    Mutex(pthread_mutex_t* pmtx) :pmtx_(pmtx)
    {}
    ~Mutex()
    {}
    void lock()
    {
        pthread_mutex_lock(pmtx_);
    }
    void unlock()
    {
        pthread_mutex_unlock(pmtx_);
    }
private:
    pthread_mutex_t* pmtx_;
};
//RAII风格的加锁方式
class lockGuard
{
public:
    lockGuard(pthread_mutex_t* pmtx) :mtx_(pmtx)
    {
        mtx_.lock();
    }
    ~lockGuard()
    {
        mtx_.unlock();
    }

private:
    Mutex mtx_;
};
// Task.hpp
#pragma once
#include <iostream>
#include <functional>

class Task
{
    // typedef std::function<int(int,int)> func_t;
    using func_t = std::function<int(int,int)>;
public:
    Task(){}
    Task(int x,int y,func_t func)
        :x_(x),y_(y),func_(func) 
    {}
    int operator()()
    {
        return x_ + y_;
    }

public:
    int x_;
    int y_;
    func_t func_;
};

POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);

上一节生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量):

基于环形队列的生产消费模型

  • 环形队列采用数组模拟,用模运算来模拟环状特性
  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
  • 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
// ringQueue.hpp
#ifndef _RING_QUEUE_HPP_
#define _RING_QUEUE_HPP_
#include <iostream>
#include <vector>
#include <pthread.h>
#include "sem.hpp"
const int g_num = 5;
// 多生产多消费的意义在哪里??
// 不在于并发的往交易场所放任务和拿任务,而在于并发的在放任务前生产任务,在拿任务后处理任务

// 信号量的本质是一个计数器 -- 计数器的意义是什么?可以不用进入临界区就可以知道临界资源的情况,甚至可以减少临界区内部的判断
// 申请锁 -> 访问与判断 -> 释放锁 -- 本质是我们不清楚临界资源的情况
// 信号量要提前预设资源的情况,而且在pv变化的过程中(原子的),我们可以在外部就知道临界资源的情况

// 多线程
template<class T>
class RingQueue
{
public:
    RingQueue(int num = g_num)
        :ring_queue_(num)
        ,space_sem(num)
        ,data_sem(0)
        ,num_(num)
        ,c_size_(0)
        ,p_size_(0)
    {
        pthread_mutex_init(&clock,nullptr);
        pthread_mutex_init(&plock,nullptr);
    }
    ~RingQueue()
    {
        pthread_mutex_destory(&clock);
        pthread_mutex_destroy(&plock);
    }
    // 生产者:空间资源,生产者们的临界资源是什么?下标
    void push(const T &out)
    {
        space_sem.p();
        pthread_mutex_lock(&plock);
        c_size_ %= num_;
        ring_queue_[c_size_++] = out;
        pthread_mutex_unlock(&plock);
        data_sem.v();
    }
    // 消费者:数据资源,消费者们的临界资源是什么?下标
    void pop(T *out)
    {
        data_sem.p();
        pthread_mutex_lock(&clock);
        p_size_ %= num_;
        *out = ring_queue_[p_size_++];
        pthread_mutex_unlock(&clock);
        space_sem.v();
    }
private:
    std::vector<T> ring_queue_;
    int num_;
    int c_size_;
    int p_size_;
    sem space_sem;  // 空间资源
    sem data_sem;   // 数据资源
    pthread_mutex_t clock;
    pthread_mutex_t plock;
};

#endif
// sem.hpp
#ifndef _SEM_HPP_
#define _SEM_HPP_
#include <semaphore.h>
class sem
{
public:
    sem(int value)
    {
        sem_init(&sem_,0,value);
    }
    ~sem()
    {
        sem_destroy(&sem_);
    }
    void v()
    {
        sem_post(&sem_);
    }
    void p()
    {
        sem_wait(&sem_);
    }


private:
    sem_t sem_;
};



#endif
// main.cc
#include "ringQueue.hpp"
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>

// 消费数据
void* consumer(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;
    while(true)
    {
        sleep(1);
        int x;
        // 1.从环形队列中获取资源或数据
        rq->pop(&x);
        // 2.进行一定的处理 - 不要忽略它消耗的时间问题
        std::cout << "consumer获得数据 x:" << x << " [" << pthread_self() << "]" << std::endl;
    }
}
// 生产数据
void* product(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;
    while(true)
    {
        // 1.构建数据或任务对象 -- 一般可以从外部来 -- 不要忽略它的时间消耗
        int x = rand() % 100 + 1;
        std::cout << "product生产数据 x:" << x << " [" << pthread_self() << "]" << std::endl;
        // 2.推送到环形队列中
        rq->push(x);// 完成生产过程
    }

}


int main()
{
    srand((unsigned int)time(nullptr) ^ getpid());
    RingQueue<int>* rq = new RingQueue<int>();
    pthread_t c[2],p[2];
    pthread_create(c,nullptr,consumer,rq);
    pthread_create(c+1,nullptr,consumer,rq);
    pthread_create(p,nullptr,product,rq);
    pthread_create(p+1,nullptr,product,rq);

    pthread_join(c[0],nullptr);
    pthread_join(c[1],nullptr);
    pthread_join(p[0],nullptr);
    pthread_join(p[1],nullptr);

    return 0;
}

多生产多消费的意义在哪里??
不在于并发的往交易场所放任务和拿任务,而在于并发的在放任务前生产任务,在拿任务后处理任务

信号量的本质是一个计数器 – 计数器的意义是什么?可以不用进入临界区就可以知道临界资源的情况,甚至可以减少临界区内部的判断
申请锁 -> 访问与判断 -> 释放锁 – 本质是我们不清楚临界资源的情况
信号量要提前预设资源的情况,而且在pv变化的过程中(原子的),我们可以在外部就知道临界资源的情况

共享资源->任何时刻只有一个执行流在访问 即 互斥是将共享资源当作一个整体使用的
那么怎么支持并发访问共享资源呢->不将共享资源当成一个整体,让不同的执行流访问不同的区域->在访问同一个区域时,在进行互斥和同步。即环形队列数组的每一个空间和阻塞队列的每一个动态申请空间。
我们同时需要解决两个问题
1.一共多少个资源,还剩多少个
2.你怎么保证这个资源是给你的?怎么知道我是否可以拥有整个共享资源?
本节我们使用环形队列就需要考虑生产者和消费者是否指向了同一个位置,是否要考虑互斥和同步。
有两种方法1.计数器 2.专门浪费一个格子
本节使用的是计数器,那我们简单说一下为什么要浪费一个格子
因为当生产消费指向同一个位置的时候,我们不知道是数据满了还是数据空了,生产者和消费者最开始是指向同一个位置,我们认为此时为空,在生产者生产时,它始终指向下一个要生产而还没生产的位置,所以如果不空格,当数据满是生产者和消费者指向的也是同一个位置,所以要空一个格子,但是空哪一个无所谓(始终是消费者前面的格子),在判断满时,生产者+1=消费者,为满
在这里插入图片描述
用信号量的方法
在这里插入图片描述
在这里插入图片描述

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值