Linux线程5——生产消费模型

生产消费模型

1个交易场所:超市 2种角色:生产者/消费者 3种关系:生产者和生产者(竞争关系也叫互斥关系),消费者和消费者(竞争关系同样是互斥关系),生产者和消费者(互斥,同步关系:生产完再消费或消费完再生产)。

以上是生产消费模型遵守的“321”原则。

生产者和消费者角色由线程承担,也就是给线程角色化。交易场所是某种数据结构表示的缓冲区。

超市中有没有新增商品只有生产者知道,超市货架有没有多余空间让生产者进行生产,这个只有消费者知道。

如何理解条件满足的时候,我们再唤醒指定的线程——我怎么知道条件是否满足呢?

当我们给线程角色化之后,当生产者生产了商品,生产者就立马意识到这个商品可以被读取了,从而通知消费者来消费。同理,消费者把数据一拿走,就知道我们又有了空间,进而可以通知生产者再来生产。所以,我们就可以让生产者线程和消费者线程互相同步,完成对应的生产消费模型。

如果只有一个生产者一个消费者,是不是只需要维护生产和消费的保证数据安全和同步的策略呢?而且不需要维护消费者和消费者之,生产者和生产者之间的同步相关特征。答案是:是的

生产者生产的数据是从哪里来的?目前不知道,但是生产这些数据一定要花时间。

消费者如何使用发送过来的数据?消费者也一样,目前不知道如何使用,但是一定要花时间处理数据。

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

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

pthread_cond_wait第二个参数是一把锁,它的意义在于:成功调用wait之后,传入的锁会被自动释放。

我们下面的push函数中就有用到pthread_cond_wait接口

当进程进行wait的之后,但之后被唤醒时,又是从哪里唤醒的呢?

从哪里阻塞挂起(从哪里等待),就从哪里唤醒,被唤醒的时候,我们还是在临界区中被唤醒的。

当我们被唤醒的时候,pthread_cond_wait,会自动帮助我们线程获取锁(即被唤醒的时候重新持有pthread_cond_wait第二个参数的这把锁)。

消费者刚拿走一个数据,生产者就可以来生产了,即生产者被唤醒可以由消费者来完成。生产者和消费者可以互相唤醒。

为什么程序里的唤醒在解锁之前或解锁之后都可以呢?

如果在解锁之前唤醒一个进程,该进程醒来后,我们不用担心锁未被释放,因为被唤醒的线程本来是在条件变量下等,现在变成了在锁上等,一旦解锁它会获得对应的锁。若多个线程都在条件变量下等,我们只唤醒了一个线程,解锁时只有这一个线程得到了锁。

条件变量使用规范

等待条件代码

pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);

给条件发送信号代码

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

生产者生产的数据是从哪里来的?目前不知道,但是生产这些数据一定要花时间。
消费者如何使用发送过来的数据?消费者也一样,目前不知道如何使用,但是一定要花时间处理数据。

生产消费模型效率提高体现在:当消费者要花很多时间处理数据时,当消费者正在处理数据的时候,他并没有访问锁,因为这个数据已经被拿到了消费者线程的上下文当中,所以,此时生产者线程可以生产数据甚至把数据放到仓库里,因此,实现了俩个线程的并发,提高了俩个线程的并发度,效率提高就体现在了这里。

我们下面写的代码既可以支持单生产,单消费又可以支持多生产,多消费,因为我们在里面有一把互斥锁,多个生产者要同时生产得先竞争锁,多个消费者同时消费也得竞争锁。

BockQueue.hpp

#pragma once
#include<iostream>
#include<queue>
#include<pthread.h>
#include<mutex>
#include<unistd.h>
#include"lockGuard.hpp"
using namespace std;
const int gDefault=5;
//#define INI_MTX(mtx) pthread_mutex_init(&mtx,nullptr)//初始化锁,这是个函数
//#define INI_COND(cond) pthread_cond_init()
template <class T>
class BlockQueue//阻塞队列
{
 private:
  bool isQueueEmpty()//封装一下判断队列是否为空
  {
   return bq_.size()==0;//如果为0,则此时队列为空
  }
  bool isQueueFull()//判断队列是否为满
  {
   return bq_.size()==capacity_;
  }
 public:
 BlockQueue(int capacity=gDefault)
 :capacity_(capacity)
 {
   pthread_mutex_init(&mtx_,nullptr);   //初始化锁
   pthread_cond_init(&Empty_,nullptr);//初始化条件变量
   pthread_cond_init(&Full_,nullptr);
 } 
 //这个push是未使用封装锁的
/* void push(const T& in)//生产者在队列中放数据
 {
    //push的时候要加锁,防止出现push还未结束,就被pop
    pthread_mutex_lock(&mtx_);
    //1.放入数据的时候要检测当前的临界资源是否能够满足访问条件
    //2.访问临界资源
    
    //pthread_cond_wait:只要是一个函数,就可能调用失败。
    //如果失败了,即线程未被成功阻塞而是继续向后走,但此时队列如果满了,往里面push内容,就会发生越界
    //pthread_cond_wait:可能存在伪唤醒的情况,即唤醒条件并未满足,就唤醒线程。
    //为了避免上面的情况发生,我们不能使用if进行判断,而是应该使用while,这样跳出while的时候,我们就一定能确定资源是就绪的。
    while(isQueueFull())
    pthread_cond_wait(&Full_,&mtx_);//如果队列满了,生产者进行等待,用Full这个条件变量去等待
   //这里等待的时候,我们还在临界区中,即我是持有锁的,如果我去等待了,也就是说我是抱着锁去等待的,但这样会出现问题:即便我去等待了,但消费者无法申请锁成功,无法进入消费,相当于我此时等待就白等待了,对于这种问题pthread_cond_wait第二个参数是一把锁,通过第二个参数可以解决这个问题,这个接口会自动释放这把锁
   //
   //进行访问临界资源
   bq_.push(in);
   if(bq_.size()>=capacity_/2)//如果生产数据超过总容量的一半就唤醒消费者。
    pthread_cond_signal(&Empty_);
    pthread_mutex_unlock(&mtx_);
 }*/


 //这个push是使用了封装锁的
 void push(const T& in)//生产者在队列中放数据
 {
 lockGuard lockguard(&mtx_);//栈上开辟的临时对象,会调用构造函数
    while(isQueueFull())
    pthread_cond_wait(&Full_,&mtx_);//如果队列满了,生产者进行等待,用Full这个条件变量去等待
   bq_.push(in);
    pthread_cond_signal(&Empty_);
    //走到这里时,会自动调用lockguard的析构函数
 }
//未封装锁的pop
/*
 void pop(T* out)//消费者拿数据
 {
   //pop的时候依然要加锁和解锁,保证线程安全
    pthread_mutex_lock(&mtx_);
    while(isQueueEmpty()) pthread_cond_wait(&Empty_,&mtx_);
    //访问临界资源
    *out=bq_.front();
    bq_.pop();
    pthread_cond_signal(&Full_);//唤醒生产者
    pthread_mutex_unlock(&mtx_);
    //这里在解锁之后唤醒或解锁之前唤醒都是可以的,只要生产者和消费者任何一个时间访问临界资源都持有锁,之前之后唤醒都不影响,因为绝对是安全的。所以,通常在之后去唤醒。
    //
 }*/

 //封装了锁的pop
 void pop(T* out)
 {
  lockGuard lockguard(&mtx_);
    while(isQueueEmpty()) pthread_cond_wait(&Empty_,&mtx_);
    //访问临界资源
    *out=bq_.front();
    bq_.pop();
  pthread_cond_signal(&Full_);  
 }
 ~BlockQueue()
 {
  pthread_mutex_destroy(&mtx_);
  pthread_cond_destroy(&Empty_);
  pthread_cond_destroy(&Full_);
 }
// mutex mtx;//C++里面的锁
 private:
 queue<T> bq_;
 int capacity_;//阻塞队列的容量上限
 pthread_mutex_t mtx_;//互斥锁来保证阻塞队列的安全,防止出现这边未上完货,那边就购买的情况
 //俩种条件场景:满和空
 pthread_cond_t Empty_; //用它来表示bq是否为空
 pthread_cond_t Full_;  //用它来表示bq是否满了
};

ConProd.cc

#include"BlockQueue.hpp"
#include"Task.hpp"
#include<pthread.h>
#include<ctime>
int myAdd(int x,int y)
{
 return x+y;
}
void *consumer(void *args)
{
 BlockQueue<Task> *bqueue=(BlockQueue<Task>*)args;
 while(true)//消费的过程
 {
 //获取任务
  Task t;
  bqueue->pop(&t);//如果没有数据了,消费者会卡在pop接口
 //完成任务
 cout<<pthread_self()<<" consumer:"<<t.x_<<"+"<<t.y_<<"="<<t()<<endl;
 sleep(1);
 }
 return nullptr;
}
void *productor(void *args)
{
 BlockQueue<Task> *bqueue=(BlockQueue<Task>*)args;
 while(true)//生产的过程
 {
 //制作任务
  int x=rand()%10+1;
  usleep(rand()%1000);
  int y=rand()%5+1;
  
  Task t(x,y,myAdd);
 //生产任务
  bqueue->push(t);//如果没有数据了,生产者会卡在push接口,把任务push给队列
  //输出信息
 cout<<pthread_self()<<"productor:"<<t.x_<<"+"<<t.y_<<"="<<t()<<endl;
 }
 return nullptr;
}
int main()
{
 srand((uint64_t)time(nullptr)^getpid()^0x32456);//生成随机数种子
//阻塞队列里放的是task任务对象,了解Task到Task.hpp中
BlockQueue<Task> *bqueue=new BlockQueue<Task>(); //阻塞队列
//单线程
/*
 pthread_t c,p;//c线程是消费,p线程是生产
 pthread_create(&c,nullptr,consumer,bqueue);
 pthread_create(&p,nullptr,productor,bqueue);

 pthread_join(c,nullptr);
 pthread_join(p,nullptr);
 delete bqueue;
 */
//多线程
 pthread_t c[2],p[2];//c线程是消费,p线程是生产
 pthread_create(c,nullptr,consumer,bqueue);
 pthread_create(c+1,nullptr,consumer,bqueue);
 pthread_create(p,nullptr,productor,bqueue);
 pthread_create(p+1,nullptr,productor,bqueue);

 pthread_join(c[0],nullptr);
 pthread_join(c[1],nullptr);
 pthread_join(p[0],nullptr);
 pthread_join(p[1],nullptr);
 delete bqueue;
 return 0;
}

Task.hpp

//这个主要用来处理数据
//我们将这里的任务设置为计算器
#pragma once
#include<iostream>
#include<functional>
using namespace std;
typedef function<int(int,int)> func_t;//返回值int,参数(int,int),func_t是我们定义的一个函数类型
class Task
{
 public:
     Task(){}//缺省构造
    Task(int x,int y,func_t func)
    :x_(x),y_(y),func_(func)
    {}
    int operator()()//重载(),这是一个仿函数
    {
     return func_(x_,y_);
    }
 public:
     int x_;
     int y_;
     func_t func_;
};

lockGuard.hpp

//封装锁
#pragma once 
#include<iostream>
#include<pthread.h>
using namespace std;
class Mutex
{
 public:
     Mutex(pthread_mutex_t *mtx)
    :pmtx_(mtx)
    {}
    void lock()
    {
    cout<<"要进行加锁了"<<endl;
     pthread_mutex_lock(pmtx_);
    }
    void unlock()
    {
    cout<<"要进行解锁了"<<endl;
     pthread_mutex_unlock(pmtx_);
    }
    ~Mutex()
    {}
 private:
     pthread_mutex_t *pmtx_;
};
class lockGuard
{
 public:
     lockGuard(pthread_mutex_t *mtx)
    :mtx_(mtx)
    {
     mtx_.lock();
    }
    ~lockGuard()
    {
    mtx_.unlock();
    }
private:
     Mutex mtx_;
};

makefile

cp:ConProd.cc
    g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
    rm -rf cp

多线程运行结果

多生产和多消费的意义:可以让生产之前和消费之后,并发的有多个执行流同时进行数据处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

头发没有代码多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值