目录
一,什么是生产者消费者模型
二,生产者消费者优点
三,基于生产者消费者的BlockQueue模型
四,基于环形队列的生产者消费者模型
五,线程池的模拟实现
一,什么是生产者消费者模型
以超市为例。
消费者是客户,生产者是供货商,超市是场所,供货商对接超市,消费者也对接超市,生产者与消费者不直接沟通,而是通过超市进行交互,当消费者去购物时,如果没有货物,留下工作人员的联系方式,当有货物时,让消费者来买,同理,供货商也是一样的道理,采用这种交易方式,避免了消费者和供货商一直不停的轮询,消耗他两的资源。
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
二,生产者消费者优点
1,支持多线程并发
2,解耦,
3,忙闲不均
遵从“321”原则
“3”,三种关系,消费者--消费者(竞争关系--互斥关系) 生产者--消费者 (竞争关系(数据的正确),同步关系(多线程协同)) 生产者--生产者(竞争关系--互斥关系)
“2” ,2种角色 生产者和消费者
“1”,一个交易场所,通常是内存中的一段缓冲区
三,基于生产者消费者的BlockQueue模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
其代码如下:
模拟实现BlockQueue
#pragma once
#include<iostream>
#include<unistd.h>
#include<queue>
#include<pthread.h>
#include<cstdlib>
#include<ctime>
#define NUM 32
using namespace std;
template<typename T>
class BlockQueue
{
private:
bool isFull()
{
return _q.size()==_cap;
}
bool isEmpty()
{
return _q.empty();
}
public:
//构造函数
BlockQueue(int cap=NUM)
:_cap(cap)
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&_full,nullptr);
pthread_cond_init(&_empty,nullptr);
}
void push(const T& in)
{
pthread_mutex_lock(&lock);
while(isFull())
{
pthread_cond_wait(&_full,&lock);
}
_q.push(in);
if(_q.size()>=_cap/2)
{
//通知其他线程
cout<<"数据已经很多了,消费者快去处理"<<endl;
pthread_cond_signal(&_empty);
}
pthread_mutex_unlock(&lock);
}
void pop(T& out)
{
pthread_mutex_lock(&lock);
while(isEmpty())
{
pthread_cond_wait(&_empty,&lock);
}
out=_q.front();
_q.pop();
if(_q.size()<=_cap/2)
{
cout<<"消费者快消费完了,生产者快生产"<<endl;
pthread_cond_signal(&_full);
}
pthread_mutex_unlock(&lock);
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&_full);
pthread_cond_destroy(&_empty);
}
private:
queue<T>_q;
int _cap;
pthread_mutex_t lock;//加锁,
pthread_cond_t _full;//同步
pthread_cond_t _empty;
};
主函数的单生产者和单消费者
#include"BlockQueue.hpp"
void *Product(void *arg)
{
auto bq=(BlockQueue<int>*)arg;
while(true)
{
sleep(1);
int data=rand()%100+1;
bq->push(data);//生产数据
cout<<"生产数据"<<data<<endl;
}
}
void *Customer(void *arg)
{
auto bq=(BlockQueue<int>*)arg;
while(true)
{
sleep(3);
int data=0;
bq->pop(data);
cout<<"消费数据"<<data<<endl;
}
}
int main()
{
//创建线程
pthread_t cust,pro;
BlockQueue<int>* bq=new BlockQueue<int>();
pthread_create(&pro,nullptr,Product,(void*)bq);
pthread_create(&cust,nullptr,Customer,(void*)bq);
pthread_join(cust,nullptr);
pthread_join(pro,nullptr);
return 0;
}
当进程运行起来后,会发现生产者一直生产数据,消费者一直在消费数据。
在实际的应用场景中,可能会有这样的情景,一个线程产生数据,另一个线程去使用这些数据运算。
而消费者生产者模型就匹配了这种场景。
四,基于环形队列的生产者消费者模型
1,POSIX信号量
posix信号量是为了同步操作,使得无冲突的并发访问共享资源,posix可以用于线程之间的同步。
初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值,表示初始信号量有多少,本质是一个计数器,
信号量等待
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
信号量发布
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
销毁信号量
int sem_destroy(sem_t *sem);
基于POSIX信号量,我们写一个可以并发操作的环形队列,
1,首先创建环形队列的类,提供push和pop接口,使得可以写数据和出数据。
遵从的原则:1,生产者和消费者不能指向同一个位置,
2,无论是生产者或是消费者,都不能把对方套一个圈。
2,创建消费者和生产者线程,访问这个共享的环形队列,访问数据,生产者写数据,消费者读数据
基于上述条件,我们写如下代码‘
创建的环形队列使用数组模拟,原因是数组支持随机访问,从而,比如:使得线程1访问0号下标的元素,线程2访问1号下标的元素,可以同时访问共享资源。
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<vector>
#include<cstdlib>
#include<semaphore.h>
#define NUM 5
using namespace std;
template<class T>
class RingQueue
{
private:
vector<T>q;
int cap;
int c_pos;//消费者的消费位置
int p_pos;//生产者生产的位置
//信号量
sem_t Blank_sem;//记录空间
sem_t Data_sem;
private:
void P(sem_t &s)//P操作,让信号--
{
sem_wait(&s);
}
void V(sem_t &s)//V操作,让信号++
{
sem_post(&s);
}
public:
//构造
RingQueue(int cap=NUM)
:cap(NUM)
,c_pos(0)
,p_pos(0)
{
q.resize(cap);
sem_init(&Blank_sem,0,NUM);
sem_init(&Data_sem,0,0);
}
//生产者调用,关心空间大小
void push(const T&data)
{
//P操作让信号量的计数器--,使的空间信号量减少
P(Blank_sem);
q[p_pos]=data;
//V操作让信号量的计数器++,使的数据信号量增加
V(Data_sem);
p_pos++;
p_pos%=cap;
}
void pop(T&data)
{
P(Data_sem);
data=q[c_pos];
V(Blank_sem);
c_pos++;
c_pos%=cap;
}
~RingQueue()
{
sem_destroy(&Blank_sem);
sem_destroy(&Data_sem);
}
};
生产者消费者的调用
#include"ring.hpp"
void *customer(void*arg)
{
auto rq=(RingQueue<int>*)arg;
while(true)
{
sleep(1);
int data=0;
rq->pop(data);
cout<<"customer "<<"消费 "<<data<<endl;
}
}
void *producter(void*arg)
{
auto rq=(RingQueue<int>*)arg;
while(true)
{
sleep(1);
int data=rand()%100+1;
rq->push(data);
cout<<"producter "<<" 生产 "<<data<<endl;
}
}
int main()
{
srand((unsigned long)time(nullptr));
RingQueue<int>*rq=new RingQueue<int>();
pthread_t tid1,tid2;
pthread_create(&tid1,nullptr,customer,(void *)rq);
pthread_create(&tid2,nullptr,producter,(void *)rq);
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
return 0;
}
然后编译运行:发现消费者和生产者可以并发的执行,从而避免了加锁单一执行的过程
可以看到,消费的数据与生产的数据不相同,源于信号量所支持的同步操作。
五,线程池的模拟实现
概念简介:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
应用场景:
1. 需要大量的线程来完成任务,且完成任务的时间比较短
2.对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3.接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
1. 创建固定数量线程池,循环从任务队列中获取任务对象,
2. 获取到任务对象后,执行任务对象中的任务接口
首先是线程池实现:
#pragma once
#include<iostream>
#include<pthread.h>
#include<queue>
#include<ctime>
#include<cstdlib>
#define NUM 5
using namespace std;
template<class T>
class ThreadPool
{
public:
ThreadPool(int cap=NUM):thread_num(cap)
{
pthread_mutex_init(&_lock,nullptr);
pthread_cond_init(&_cond,nullptr);
}
void LockQueue()
{
pthread_mutex_lock(&_lock);
}
void UnlockQueue()
{
pthread_mutex_unlock(&_lock);
}
void Wait()
{
pthread_cond_wait(&_cond,&_lock);
}
void Wake()
{
pthread_cond_signal(&_cond);
}
bool isQueueEmpty()
{
return _task_queue.size()==0?true:false;
}
//为什么定义成静态成员:为了不让外部调用这个函数,避免了this指针的调用问题
static void*Rountine(void *arg)
{
pthread_detach(pthread_self());
ThreadPool*self=(ThreadPool*)arg;
//然后判断队列是否为空,循环等待,避免被意外唤醒
while(true)
{
self->LockQueue();
if(self->isQueueEmpty())
{
self-> Wait();
}
//任务队列有任务
T t;
self->Pop(t);
self->UnlockQueue();
t.run();
}
}
void Thread_pool_init()
{
pthread_t tid;
for(int i=0;i<thread_num;i++)
{
pthread_create(&tid,nullptr,Rountine,this);
}
}
void push(const T& in)
{
LockQueue();
_task_queue.push(in);
UnlockQueue();
Wake();
}
void Pop ( T &out)
{
out=_task_queue.front();
_task_queue.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
private:
pthread_mutex_t _lock;
pthread_cond_t _cond;
queue<T>_task_queue;
int thread_num;
};
2,任务类的实现
#pragma once
#include<pthread.h>
#include<iostream>
using namespace std;
class Task
{
public:
Task()
{}
Task(int x,int y,char op)
:_x(x)
,_y(y)
,_op(op)
{}
void run()
{
int z=0;
switch(_op)
{
case'+':
z=_x+_y;
break;
case'-':
z=_x-_y;
break;
case'*':
z=_x*_y;
break;
case'/':
if(_y==0)cerr<<"div zero"<<endl;
if(_y!=0)z=_x/_y;
break;
case'%':
if(_y==0)cerr<<"mod zero"<<endl;
if(_y!=0)z=_x%_y;
break;
default:
cerr<<"operator error"<<endl;
break;
}
cout<<"thread: ["<<pthread_self()<<"]"<<_x<< _op <<_y<<" = "<< z <<endl;
}
~Task()
{}
private:
int _x;
int _y;
char _op;
};
主函数调用:
#include"Thread_pool.hpp"
#include"Task.hpp"
#include<unistd.h>
//懒汉模式
//在调用之前 先把这个使用对象的类构造起来,提供全局接口,供main函数调用
int main()
{
ThreadPool<Task>*tp=new ThreadPool<Task>();
tp->Thread_pool_init();
srand((unsigned long)time(nullptr));
const char*op="+-*/%";
while(true)
{
int x=rand()%100+1;
int y=rand()%100+1;
Task t(x,y,op[x%5]);
tp->push(t);
sleep(1);
}
return 0;
}
当任务执行起来以后,就可以看到线程都在执行各自的任务然后返回结果,
六,读者写者模型
1,读写锁
在我们编写多线程的程序中,有常见的一种情况,修改公共数据的地方很少或基本没有,而读取数据的时候却很多,通过读取数据,拷贝数据,当在这种情况下,如果在这种情况下,是不是有专门的的锁来处理这种情况,设置“读优先”,答案是有的。
a,读写优先锁的关键字都是pthread_rwlock_t ,申请锁,
其锁的使用方法如下图所示,
实际上读写锁的实现是通过计数器来实现,通过一个全局的变量来记录当前需要读或者写的线程,对计数器进行操作时,通过加锁,读取一个,计数器-1,通过这样即可实现读的时候也是原子。
同理,写的时候也是一样的思路。
//读优先锁
pthread_rwlock_t rlock;
pthread_rwlock_init(&rlock,nullptr);
//读加锁
pthread_rwlock_tryrdlock(&rlock);
//解锁
pthread_rwlock_unlock(&rlock);
pthread_rwlock_destroy(&rlock);
//写优先
pthread_rwlock_t wlock;
pthread_rwlock_init(&wlock,nullptr);
//写加锁
pthread_rwlock_trywrlock(&wlock);
//解锁
pthread_rwlock_unlock(&wlock);
pthread_rwlock_destroy(&wlock);
2,读者写者也遵从“321”原则
三种关系:读者-读者(毫无关系)读者-写者(互斥关系,同步关系)写者-写者(互斥关系)
二种角色:读者--写者
一个交易场所:内存中的一段缓冲区。