生产者消费者模型

特点

"321原则"

3:3种关系:生产者之间,消费者之间,生产者和消费者之间

  • 生产者之间:互斥关系
  • 消费者之间:互斥关系
  • 生产者和消费者之间:互斥&&同步关系

2:2种角色:生产者和消费者

1:一个交易场合

生产者和消费者模型就好比去超市买东西:

有了交易场所,生产者就可以把生产多的货,存储到交易场所中,即使生产者不生产了,只要交易场所中有货,那么消费者可以直接从交易场所中取货。

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

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

优点

  • 解耦:生产者和消费者完全独立,互不依赖
  • 支持并发
  • 支持忙闲不均

基于BlockingQueue的生产者消费者模型

BlockingQueue

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

C++ queue模拟阻塞队列的生产消费模型

代码:

我们可以对锁和条件变量进行封装,可以让我们语言变得简洁:

Mutex.hpp

复制代码
#pragma once
#include<pthread.h>
#include<mutex>

class Mutex
{
public:
Mutex()
{
    pthread_mutex_init(&_lock,nullptr);
}
void Lock()
{
    pthread_mutex_lock(&_lock);
}
void Unlock()
{
    pthread_mutex_unlock(&_lock);
}
~Mutex()
{
    pthread_mutex_destroy(&_lock);
}
pthread_mutex_t* Get()
{
    return &_lock;
}
private:
pthread_mutex_t _lock;
};
class LockGuard
{
public:
LockGuard(Mutex*lock):_lock(lock)
{
    _lock->Lock();
}
~LockGuard()
{
    _lock->Unlock();
}
    private:
    Mutex*_lock;
};

Cond.hpp

复制代码
#pragma once
#include <pthread.h>
#include <iostream>
#include "Mutex.hpp"

class Cond
{
public:
    Cond()
    {
        pthread_cond_init(&_cond, nullptr);
    }
    void NotifyAll()
    {
        int n = pthread_cond_broadcast(&_cond);
        (void)n;
    }
    void NotifyOne()
    {
        int n = pthread_cond_signal(&_cond);
        (void)n;
    }
    void Wait(Mutex &lock)
    {
        pthread_cond_wait(&_cond, lock.Get());
    }
    ~Cond()
    {
        pthread_cond_destroy(&_cond);
    }

private:
    pthread_cond_t _cond;
};

BlockQueue.hpp

复制代码
#pragma once
#include<iostream>
#include<pthread.h>
#include<queue>
#include<string>
#include<unistd.h>
#include<mutex>
#include"Cond.hpp"
#include"Mutex.hpp"
const static uint32_t gcap=5;
template<typename T>
class BlockQueue
{
    private:
    bool IsFull()
    {
        return _bq.size()>=_cap;
    }
    bool IsEmpty()
    {
        return _bq.empty();
    }
public:
    BlockQueue(uint32_t cap=gcap)
    :_cap(cap)
    ,_c_wait_num(0)
    ,_p_wait_num(0)
    {
    
    }
    ~BlockQueue()
    {
    }
    void Pop(T*out)
    {
        LockGuard lock(&_lock);
        while(IsEmpty())
        {
            _c_wait_num++;
            _c_cond.Wait(_lock);
            _c_wait_num--;
        }
        *out=_bq.front();
        _bq.pop();
        if(_p_wait_num>0)
        {
            _p_cond.NotifyAll();
        }
    }
    //进队列
    void Push(const T&in)
    {
        //上锁
       LockGuard lock(&_lock);
        while(IsFull())//保证健壮性
        {
            ++_p_wait_num;
            _p_cond.Wait(_lock);
            _p_wait_num--;
        }
        //不满的
        _bq.push(in);//完成生产
        if(_c_wait_num>0)
        {
            _c_cond.NotifyOne();
        }

    }
private:
    uint32_t _cap;//容量
    std::queue<T> _bq;//阻塞队列,即存储生产的东西
    Mutex _lock;//锁
    Cond _c_cond;//消费者用的条件变量
    Cond _p_cond;//生产者用的条件变量
    int _c_wait_num;//消费者数量
    int _p_wait_num;//生产者数量
};

当然,阻塞队列中不单单可以放整型,浮点型,字符串等等,还可以放任务,这样就是线程池的原理。

Task.hpp

复制代码
#pragma once
#include<unistd.h>
#include<iostream>
#include<functional>
using func_t=std::function<void()>;
void PrintLog()
{
    std::cout<<"我是一个日志任务"<<std::endl;
}

main.cc

复制代码
#include"BlockQueue.hpp"
#include"Task.hpp"
#include<iostream>
struct Data
{
    std::string name;
    BlockQueue<func_t>*q;
};
void* comsumer(void* arg)
{
    Data*td=static_cast<Data*>(arg);
    while(true)
    {
        sleep(1);
        func_t t;
        td->q->Pop(&t);
        t();

    }
    return (void*)0;
}
void* producer(void* arg)
{
    Data*td=static_cast<Data*>(arg);
    //int i=1, j=0;
    while(true)
    {
        sleep(1);
        td->q->Push(PrintLog);
        std::cout<<"生产了一个任务"<<std::endl;
    }
    return (void*)0;
}
//生产者消费者模型
int main()
{
    BlockQueue<func_t> *bq=new BlockQueue<func_t>();
     //单消费者,单生产者
    // pthread_t c;
    // pthread_t p;
    // Data ctd={"comsumer",bq};
    // Data ptd={"producer",bq};
    // pthread_create(&c,nullptr,comsumer,(void*)&ctd);
    // pthread_create(&p,nullptr,producer,(void*)&ptd);
    // pthread_join(c,NULL);
    // pthread_join(p,NULL);
    //多消费者,多生产者
    pthread_t c[3];
    pthread_t p[2];
    Data ctd={"comsumer",bq};
    Data ptd={"producer",bq};
    pthread_create(c,nullptr,comsumer,(void*)&ctd);
    pthread_create(c+1,nullptr,comsumer,(void*)&ctd);
    pthread_create(c+2,nullptr,comsumer,(void*)&ctd);
    pthread_create(p,nullptr,producer,(void*)&ptd);
    pthread_create(p+1,nullptr,producer,(void*)&ptd);
    pthread_join(*c,NULL);
    pthread_join(*p,NULL);
    delete bd;
    return 0;
}

POSIX信号量

概念

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

同步本质:在任意时刻,都只有一个执行流在临界区中。

信号量主要统计可用资源的数量。

比如一家电影院,里面的位置有100个,电影院是买了票的才有资格进入,电影院是个临界区,那么里面的100个就是可以进入该临界区享受临界资源的人,而这一百个座位就是信息量。电影院里的位置是可以预定的,当有人预定的时候,电影院的位置就少一个,即信息量也少一个,当有人离开电影院的时候,电影院的位置就多一个。当然,买了票的人可以一次多人进入,那么线程进入临界区也可以多个线程同时进入,但是最多可以进入信息量中统计的可用资源的数量。

总结:

  • 总共有100个座位 = 信号量初始值是100
  • 每卖出一张票 = 信号量减1(P操作)
  • 每有观众离场 = 信号量加1(V操作)
  • 座位卖光了 = 信号量为0,新观众要等待

有关信息量的函数:

初始化信息量

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

pshared:0表示线程间共享,非零表示进程间共享

value:信号量初始值

销魂信息量

int sem_destroy(sem_t *sem);

等待信号量(P操作)

功能:申请信号量,会将信号量的值减1

int sem_wait(sem_t *sem);

发布信号量(V操作)

功能:释放信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

int sem_post(sem_t *sem);

封装信号量

复制代码
#pragma once
#include<semaphore.h>
class Sem
{
    public:
    Sem(int num):_initnum(num)
    {
        sem_init(&_sem,0,_initnum);
    }
    ~Sem()
    {
        sem_destroy(&_sem);
    }
    void P()//申请信息量
    {
        int n=sem_wait(&_sem);
        (void)n;
    }
    void V()//释放信号量
    {
        int n=sem_post(&_sem);
        (void)n;
    }
private:
sem_t _sem;
int _initnum;//初始化数量
};

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

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

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

当数据存放到尾的下一个的时候,通过index%N来实现环形。

  • 为空为满的时候,首尾的下标一样。
  • 不为空不为满的时候,首尾的下标不一样。

但如何分清为空和为满状态呢?

给该数组多一个空间,当(tail + 1) % size == head时候,该环形队列为满,当首尾下标一样的时候,该环形队列为空。

现在我们用这个信息量这个计数器就可以实现。

信息量可以知道是否还有资源、空间可以用。

代码:

复制代码
#ifndef _RINGQUEUE_HPP_
#define _RINGQUEUE_HPP_

#include "Sem.hpp"
#include <iostream>
#include <pthread.h>
#include <vector>
#include "Mutex.hpp"

const int gcap = 5;

template <typename T>
class RingQueue
{
public:
    RingQueue(int cap = gcap)
        : _ringQueue(cap), _cap(cap), _space_sem(cap), _data_sem(0)
    , _c_step(0), _p_step(0)
    {
    }
    void Push(const T&in)
    {
        _space_sem.P();
        {
            LockGuard lockguard(&_p_lock);
            _ringQueue[_p_step++]=in;
            _p_step%=_cap;
        }
        _data_sem.V();
    }
    void Pop(T*out)
    {
        _data_sem.P();
        {
            LockGuard lockguard(&_c_lock);
            *out=_ringQueue[_c_step++];
            _c_step%=_cap;
        }
        _space_sem.V();
    }
    ~RingQueue()
    {
    }
private:
    std::vector<T> _ringQueue;
    int _cap;

    Sem _space_sem; // 管理空间的信息量
    Sem _data_sem;  // 管理数据的信息量

    Mutex _c_lock; // 消费者的锁
    Mutex _p_lock; // 生产者的锁

    int _c_step; // 消费者的位置
    int _p_step; // 生产者的位置
};
#endif
相关推荐
阿巴~阿巴~37 分钟前
HTTP进化史:从0.9到3.0的技术跃迁
linux·服务器·网络·网络协议·http
列逍38 分钟前
Linux进程(一)
linux·运维·服务器
Blossom.11841 分钟前
基于Qwen2-VL+LayoutLMv3的智能文档理解系统:从OCR到结构化知识图谱的落地实践
开发语言·人工智能·python·深度学习·机器学习·ocr·知识图谱
FuckPatience41 分钟前
C# 补码
开发语言·算法·c#
小年糕是糕手43 分钟前
【C++】类和对象(五) -- 类型转换、static成员
开发语言·c++·程序人生·考研·算法·visual studio·改行学it
星释43 分钟前
Rust 练习册 106:太空年龄计算器与宏的魔法
开发语言·后端·rust
Xの哲學44 分钟前
Linux内核数据结构:设计哲学与实现机制
linux·服务器·算法·架构·边缘计算
diegoXie44 分钟前
PCRE Lookaround (零宽断言)总结(R & Python 通用)
开发语言·python·r语言
任子菲阳1 小时前
学Java第五十二天——IO流(下)
java·开发语言·intellij-idea