生产消费模型
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
多线程运行结果
多生产和多消费的意义:可以让生产之前和消费之后,并发的有多个执行流同时进行数据处理。