Linux-生产消费模型

上一篇,我们讲到了关于互斥的知识点,那现在,我们就来继续学习一下关于生产消费模型的知识点!

在开始列出它的概念之前,我们用生活中的例子来具体形象理解生产消费模型到底是一个什么东西?

什么是生产消费模型?

总结:

我们通常称生产消费模型为:PC问题:consumer productor 分别取它们的首字母!

1.简单记口诀----“321原则”

3种关系:生产者与生产者、消费者与消费者、生产者与消费者。

2种角色:生产、消费

1个交易场所:特定结构的内存空间

2.优点:(虽然它们各自都要花时间处理自己的事,但是有以下优势:)

1)支持忙闲不均

2)生产和消费进行解耦

3)支持并发

现在,我们再来看概念:

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

生产者消费者模式就是通过一个容器(超市)来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

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

多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞直到队列中被放入了元素当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

现在,我们来简单用代码实现一下:

代码实现:

阻塞队列部分

template<class T>
class BlockQueue
{
    static const int defaultNum=30;
public:
    BlockQueue(int maxCapacity=defaultNum)
        :_maxCapacity(maxCapacity)
    {
        pthread_mutex_init(&mtx,nullptr);
        pthread_cond_init(&p_cond,nullptr);
        pthread_cond_init(&c_cond,nullptr);
    }

    T Pop()
    {
        pthread_mutex_lock(&mtx);
        //当队列为空时阻塞
        while(_q.size()==0) //因为判断临界资源调试是否满足,也是访问临界资源,判断资源是否就绪,是通过资源内部判断的
        {
            //如果线程wait时,被唤醒误了,也不怕,因为你是持有锁的,自动释放锁,因为唤醒而返回的时候,会重新持有锁。
            //队列满的时候,消费者就进行等待
            pthread_cond_wait(&c_cond,&mtx);
        }
        // T*out=_q.top();
        T out=_q.front();
        _q.pop();
        //删去队列的数据后,就唤醒生产者
        pthread_cond_signal(&p_cond);
        pthread_mutex_unlock(&mtx);
        return out;
    }

    void Push(const T&in)
    {
        pthread_mutex_lock(&mtx);
        //当队列为满的时候,阻塞
        while(_q.size()==_maxCapacity)
        {
            //队列满的时候,生产者就进行等待
            pthread_cond_wait(&p_cond,&mtx);
        }
        _q.push(in);
        //生产者插入有数据后,就唤醒消费者
        pthread_cond_signal(&c_cond);
        pthread_mutex_unlock(&mtx);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&mtx);
        pthread_cond_destroy(&p_cond);
        pthread_cond_destroy(&c_cond);
    }
private:
    std::queue<T> _q;
    int _maxCapacity;//最值
    pthread_mutex_t mtx;
    pthread_cond_t p_cond;
    pthread_cond_t c_cond;    
};

模拟加减乘除任务

#pragma once
#include <string>

const std::string opers = "+-*/%";
enum
{
    Divzero = 1,
    Modzero = 2,
    Unknow
};

class Task
{
public:
    Task()
    {}
    Task(int x, int y, char op)
        : data1(x), data2(y), oper(op), result(0), exitcode(0)
    {
    }

    void run()
    {
        switch (oper)
        {
        case '+':
            result = data1 + data2;
            break;
        case '-':
            result = data1 - data2;
            break;
        case '*':
            result = data1 * data2;
            break;
        case '/':
            if (data2 == 0)
                exitcode = Divzero;
            else
                result = data1 / data2;
            break;

        case '%':
            if (data2 == 0)
                exitcode = Modzero;
            else
                result = data1 % data2;
            break;
        default:
            exitcode = Unknow;
            break;
        }
    }

    bool operator()()
    {
        run();
        return true;
    }

    std::string GetResult()
    {
        std::string r = std::to_string(data1);
        r += oper;
        r += std::to_string(data2);
        r += '=';
        r += std::to_string(result);
        r += "[code:";
        r += std::to_string(exitcode);
        return r;
    }

    std::string GetTask()
    {
        std::string r = std::to_string(data1);
        r += oper;
        r += std::to_string(data2);
        r += "= ?";
        return r;
    }

    ~Task()
    {
    }

private:
    int data1;
    int data2;
    char oper;

    int result;
    int exitcode;
};

实验生产与消费模型:

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <unistd.h>
// 消费者
void *Consumer(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        // 消费
        Task t = bq->Pop();
        // t->run();
        t();
        // 计算
        std::cout << "处理任务: " << t.GetTask() << " 运算结果是: " << t.GetResult() << " thread id: " << pthread_self() << std::endl;
        //注意:这里要加短暂休眠。具体原因:看下面
        usleep(1000);
    }
    // return nullptr;
}

// 生产者
void *Producer(void *args)
{
    int len = opers.size();
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        int data1 = rand() % 10 + 1;
        usleep(10);
        int data2 = rand() % 10;
        char op = opers[rand() % len];
        Task t(data1, data2, op);
        bq->Push(t);
        std::cout << "生产了一个任务: " << t.GetTask() << " thread id: " << pthread_self() << std::endl;
        sleep(1);
    }
    // return nullptr;
}

int main()
{
    srand(time(nullptr));
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    pthread_t c[3], p[5];

    for (int i = 0; i < 3; i++)
    {
        pthread_create(c+i, nullptr, Consumer, bq);
    }

    for (int i = 0; i < 5; i++)
    {
        pthread_create(p+i, nullptr, Producer, bq);
    }

    for (int i = 0; i < 3; i++)
    {
        pthread_join(c[i], nullptr);
    }

    for (int i = 0; i < 5; i++)
    {
        pthread_join(p[i], nullptr);
    }
    delete bq;

    return 0;
}

上面可以看到,当你不加短暂休眠后,它会生产者处于阻塞状态。并且看消费者的线程id:只有一个线程调度?

这也很正常:

操作系统的线程调度是“抢占式”的,会根据线程优先级、时间片分配、内核调度策略等因素选择线程执行。当前场景中:
- 你创建了3个消费者线程,但其中1个线程被优先分配了时间片,持续获取到锁并处理任务;
- 其他线程处于“就绪态”,但暂时未被调度到(后续若当前线程时间片耗尽,其他线程会开始工作)。

因此,我们加了短暂休眠,目的就是可以通过增加任务处理的“耗时”(模拟实际场景的计算/IO操作),让当前线程主动让出CPU,触发其他线程的调度。

回顾信号量:

我们之前讲到过信号量:

https://blog.youkuaiyun.com/go_bai/article/details/154755520?spm=1001.2014.3001.5501

知道,信号量的本质是一把计数器,那么这把计数器的本质是什么?

本质是用来描述资源数目的,把资源是否就绪放在了临界区之外,申请信号量时,其实就间接的已经在做判断了!

PV原子性:P--,V++。

流程:
1.P()
2.访问资源
3.V()

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

P(生产者)关注什么资源?还有多少剩余空间 SpaceSem:N

C(消费者)关注什么资源?还有多少剩余空间 DataSem:0

现在,我们用图片来显示什么是环形队列的生产消费模型:

了解POSIX信号量的接口

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

信号量初始化

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

成功返回0,否则-1,设置错误码

信号量销毁

等待信号量

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

发布信号量

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

现在,我们来实现一下基于环形队列的生产消费模型代码:

循环队列部分:

#pragma once
#include <iostream>
#include<vector>
#include<pthread.h>
#include<semaphore.h>
template<class T>
class RingQueue
{
    static const int defaultNum=20;
private:
    //--
    void P(sem_t& sem)
    {
        //wait
        sem_wait(&sem);
    }
    //++
    void V(sem_t&sem)
    {
        //post
        sem_post(&sem);
    }

    void Lock(pthread_mutex_t& mtx)
    {
        pthread_mutex_lock(&mtx);
    }
    void Unlock(pthread_mutex_t&mtx)
    {
        pthread_mutex_unlock(&mtx);
    }
public:
    //记得初始化vector
    RingQueue(int maxCapacity=defaultNum)
        :_ringQueue(maxCapacity),_maxCapacity(maxCapacity),c_step(0),p_step(0)
    {
        sem_init(&cdata_sem,0,0);
        sem_init(&pspace_sem,0,maxCapacity);
        pthread_mutex_init(&c_mtx,nullptr);
        pthread_mutex_init(&p_mtx,nullptr);
    }

    void Push(const T&in) //生产
    {   
        P(pspace_sem);
        Lock(p_mtx);
        _ringQueue[p_step]=in;
        p_step++;
        p_step%=_maxCapacity;
        Unlock(p_mtx);
        // std::cout<<"Push Unlock?"<<std::endl;
        V(cdata_sem);
    }
    void Pop(T*out) //消费
    {
        P(cdata_sem);
        Lock(c_mtx);
        *out=_ringQueue[c_step];
        c_step++;
        c_step%=_maxCapacity;

        Unlock(c_mtx);
        V(pspace_sem);
    }
    ~RingQueue()
    {
        sem_destroy(&cdata_sem);
        sem_destroy(&pspace_sem);

        pthread_mutex_destroy(&c_mtx);
        pthread_mutex_destroy(&p_mtx);
    }
private:
    std::vector<T> _ringQueue;
    int _maxCapacity;
    //生产者与消费者下标
    int c_step;
    int p_step;
    //消费者关注的数据资源
    sem_t cdata_sem;
    //生产者关注的空间资源
    sem_t pspace_sem;

    //锁
    pthread_mutex_t c_mtx;
    pthread_mutex_t p_mtx;

};

测试生产与消费模型:

#include <iostream>
#include "RingQueue.hpp"
#include "Task.hpp"
#include <unistd.h>
struct ThreadData
{
    RingQueue<Task> *rq;
    std::string pathname;
};
void *Cosumer(void *args)
{
    // std::cout << "enter cosumer" << std::endl;
    ThreadData *td = static_cast<ThreadData *>(args);
    RingQueue<Task> *rq = td->rq;
    std::string name = td->pathname;
    // std::cout << "will enter while(cosumer)" << std::endl;

    while (true)
    {
        Task t;
        rq->Pop(&t);
        t();
        std::cout << "Consumer get task, task is : " << t.GetTask() << " who: " << name << " result: " << t.GetResult() << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *Productor(void *args)
{
    // std::cout << "enter productor" << std::endl;
    ThreadData *td = static_cast<ThreadData *>(args);
    RingQueue<Task> *rq = td->rq;
    std::string name = td->pathname;
    int len = opers.size();
    // std::cout << "will enter while(productor)" << std::endl;

    while (true)
    {
        int data1 = rand() % 10 + 1;
        int data2 = rand() % 10;
        char op = opers[rand() % len];
        Task t(data1, data2, op);
        // std::cout << "Task create done" << std::endl;

        rq->Push(t);

        std::cout << "Productor task done, task is : " << t.GetTask() << " who: " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    srand(time(nullptr));
    RingQueue<Task> *rq = new RingQueue<Task>(50);
    pthread_t c[5], p[3];
    for (int i = 0; i < 5; i++)
    {
        ThreadData *td = new ThreadData();
        td->rq = rq;
        td->pathname = "Conssumer" + std::to_string(i);
        pthread_create(c + i, nullptr, Cosumer, td);
    }
    // std::cout << "Conssumer thread done?" << std::endl;
    for (int i = 0; i < 3; i++)
    {
        ThreadData *td = new ThreadData();
        td->rq = rq;
        td->pathname = "Productor" + std::to_string(i);
        pthread_create(p + i, nullptr, Productor, td);
    }

    for (int i = 0; i < 5; i++)
    {
        pthread_join(c[i], nullptr);
    }
    for (int i = 0; i < 3; i++)
    {
        pthread_join(p[i], nullptr);
    }
    delete rq;
    return 0;
}

Task部分:跟上面一样

呈现效果:

总结:

1.环形队列采用数组模拟,用模运算来模拟环状特性

2.环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位(信号量)来判断满或者空。另外也可以预留一个空的位置,作为满的状态

好了,关于生产与消费模型就分享到这里了,希望大家一起进步!

最后,到了本次鸡汤环节:

如果你相信自己,你可以做任何事情!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值