一、生产者消费者模型
1、为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者消费者的强耦合问题。
生产者消费者彼此之间不再直接通讯,
而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接仍给阻塞队列,消费者不找生产者要数据,而是直接通过阻塞队列拿数据,阻塞队列相当于一个缓存区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
2、模型满足的关系
在计算机中生产者消费者具体指的是:
生产者:线程
消费者:线程
空间、交易场所:一块"内存块"
满足以下几点:
三种关系:
生产者vs生产者(互斥)、消费者vs消费者(互斥)、生产者vs消费者(同步)。
两种角色:生产者、消费者。
一个场所:一个"内存块"。
方便记忆:可以记为321原则。
//add.c
#include<stdio.h>
int add(int a,int b){
return a+b;
}
int main(){
int a=10,b=20;
int c=add(a,b);
printf("a + b = %d\n",c);
return 0;
}
对于以上代码,main函数可以看作生产者和消费者。add函数也看作生产者和消费者。main函数生产了a和b
两个数据,add作为消费者计算了a+b的值。add函数通过返回a+b的值这时add函数作为生产者,main函数打印add返回的结果这时main函数作为消费者。对于以上程序存在以向两个问题:
1、main函数和add函数都是串行的,
不支持并发
,如果add函数执行很久,那么对于main函数就会阻塞等待add函数返回结果。这样效率是不高的。
2、如果add函数出现了问题,对于整个程序就会出现问题,说明耦合性太高。
二、基于条件变量BlockingQueue的生产者消费者模型
1、BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种用于实现生产者消费者模型的数据结构。其与普通的队列区别在与,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列为满时,往队列里放入元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
2、C++ queue模拟阻塞队列的生产消费模型
我们将上面的add.c代码用单生产者消费者模型来实现,一个线程往阻塞队列中放数据,然后一个线程往阻塞队列中取数据:
为了便于理解我们实现单生产者,单消费者来进行代码编写。
3、C++代码实现:
//BlockQueue
#pragma once
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<queue>
#include<stdlib.h>
class Task{
public:
Task(int _x,int _y):x(_x),y(_y)
{}
Task()=default;
~Task()
{}
int Run(){
return x + y;
}
public:
int x;
int y;
};
class BlockQueue{
private:
std::queue<Task> q;
size_t cap;
pthread_mutex_t lock;
pthread_cond_t c_cond; // 当消费者条件不满足时,在该条件下等
pthread_cond_t p_cond; // 当生产者条件不满足时,在该条件下等
public:
bool IsFull(){
return q.size()>=cap;
}
bool IsEmpty(){
return q.empty();
}
void LockQueue(){
pthread_mutex_lock(&lock);
}
void UnLockQueue(){
pthread_mutex_unlock(&lock);
}
void WakeUpProductor(){
pthread_cond_signal(&p_cond);
}
void WakeUpConsumer(){
pthread_cond_signal(&c_cond);
}
void ProductorWait(){
pthread_cond_wait(&p_cond,&lock); //为什么等待的时候要带有一把锁:1、离开的时候释放锁,让其他线程改变条件让自己满足条件。
// 2、当条件满足时返回函数继续执行,回到了临界区,临界区必须保证原子,所以重新持有锁。这些都是函数自动完成的。
}
void ConsumerWait(){
pthread_cond_wait(&c_cond,&lock);
}
public:
BlockQueue(size_t _cap):cap(_cap)
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&c_cond,nullptr);
pthread_cond_init(&p_cond,nullptr);
}
~BlockQueue(){
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&c_cond);
pthread_cond_destroy(&p_cond);
}
//生产者生产数据
void PutTask(Task &t){
LockQueue();
while(IsFull()){ //防止消费者唤醒失败,那么这时消费者没有被唤醒,所以用while可以进行多次判断,这样能够确保消费者被唤醒。
WakeUpConsumer();
ProductorWait();
}
q.push(t);
UnLockQueue();
}
//消费者拿数据
void TakeTask(Task &t){
LockQueue();
while(IsEmpty()){
WakeUpProductor();
ConsumerWait();
}
t=q.front();
q.pop();
UnLockQueue();
}
};
//main.cpp
#include "BlockQueue.hpp"
using namespace std;
void* productor_run(void* args){
BlockQueue *bp=(BlockQueue*)args;
while(true){
int x=rand()%10+1;
int y=rand()%100+1;
Task t(x,y);
bp->PutTask(t);
cout<<"product Task is : "<<x<<" + "<<y<<" = ?"<<endl;
}
}
void *consumer_run(void *args){
BlockQueue *bp=(BlockQueue*)args;
while(true){
Task t;
bp->TakeTask(t);
cout<<"consumer Task is : "<<t.x<<" + "<<t.y<<" = "<<t.Run()<<endl;
sleep(1);
}
}
int main(){
BlockQueue *bp=new BlockQueue(5);
pthread_t p,c;
pthread_create(&p,nullptr,productor_run,(void*)bp);
pthread_create(&c,nullptr,consumer_run,(void*)bp);
pthread_join(p,nullptr);
pthread_join(c,nullptr);
delete bp;
return 0;
}
//Makefile
mybin:main.cpp
g++ $^ -o $@ -lpthread -std=c++11
.PHONY:clean
clean:
rm -rf mybin
运行结果:
1、生产者快,消费者慢:首先生产者很快的将任务队列塞满,然后消费者慢慢的将队列中的任务全部取出消费完后,重复上述两个步骤。
2、生产者慢,消费者快:首先生产者慢慢的将任务队列塞满,然后消费者很快的将队列中的任务全部取出消费完后,重复上述两个步骤。
总结:
1、生产者线程每次随机产生两个数据把数据打包成任务,把任务塞到管道中,当把定义的管道容量塞满之后,生产者线程通知消费者取任务然后自己阻塞。
2、消费者取出任务执行任务Run方法,把得到的数据返回。当把管道中的任务读空之后通知生产者生产数据然后自己阻塞。
4、生产者消费者优点
1、实现解耦
2、支持并发
3、支持忙闲不均
三、基于信号量RingQueue的生产者消费者模型
1、基于环形队列的生产消费模型
环形队列采用数组来模拟,用模运算来模拟环状特性。
环状结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过
加计数器
或者标记位来判断满或者空。另外也可以预留一个空位置,作为满的状态。
我们现在用信号量来作为计数器,就很简单的进行多线程间的同步过程,我们可以设定两个信号量:1、一个sem_blank是生产者关心的空格子信号量,初始化为最大。2、另外一个是sem_data是消费者关心的数据信号量,初始化为0。其中生产者的任务是往空格子中生产数据。消费者的任务是取出已经存在数据资源的格子。
2、C++代码实现:
//RingQueue.hpp
#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<pthread.h>
#include<unistd.h>
#define NUM 10
class RingQueue{
private:
std::vector<int> v;
int max_cap;
sem_t sem_blank;//空格子信号量
sem_t sem_data;//数据信号量
int c_index;
int p_index;
private:
void P(sem_t &s){
sem_wait(&s);
}
void V(sem_t &s){
sem_post(&s);
}
public:
RingQueue(int _cap=NUM):v(_cap),max_cap(_cap){//初始化队列和容量
sem_init(&sem_blank,0,_cap);//初始化,格子资源为最大容量
sem_init(&sem_data,0,0);//此时数据资源为0
c_index=0;
p_index=0;
}
//消费者往队列中拿数据
void Get(int &out){
P(sem_data);//当还有数据的时候,数据份数减1
out=v[c_index];
V(sem_blank);//消费一份数据,空格子的份数加1
c_index=(c_index+1)%max_cap;
}
//生产者往队列中生产数据
void Put(const int &in){
P(sem_blank);//当还有空格子的时候,格子份数减1
v[p_index]=in;
V(sem_data);//此时生产了一份数据,数据个数加1
p_index=(p_index+1)%max_cap;
}
~RingQueue(){
sem_destroy(&sem_blank);
sem_destroy(&sem_data);
c_index=0;
p_index=0;
}
};
//main.cpp
#include "RingQueue.hpp"
void *consumer(void *args){
RingQueue *rq=(RingQueue*)args;
while(true){
int data=0;
rq->Get(data);
std::cout<<"consumer get a data #: "<<data<<std::endl;
}
}
void *productor(void *args){
RingQueue *rq=(RingQueue*)args;
int count=100;
while(true){
rq->Put(count);
count++;
if(count>110){
count=100;
}
std::cout<<"productor done ..."<<std::endl;
}
}
int main(){
pthread_t p,c;
RingQueue *rq=new RingQueue();
pthread_create(&p,nullptr,consumer,rq);
pthread_create(&c,nullptr,productor,rq);
pthread_join(p,nullptr);
pthread_join(c,nullptr);
return 0;
}
//Makefile
mybin:main.cpp
g++ $^ -o $@ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf mybin
四、线程池
1、线程池什么
线程池是一组线程资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果需要相关资源直接从池中获取,无须动态分配。
2、线程池优点
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建和销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
3、线程池应用场景
- 3.1、需要大量的线程来完成任务,且完成任务的时间比较短。WEB服务器完成网站服务这样的请求,使用线程池这样的技术非常合适。因为单个任务小,而且任务数量巨大,比如热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 3.2、对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 3.3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
4、使用线程池场景图
5、C++代码实现
1、我们实现一个简单的线程池,用一个类来模拟服务器不断往任务队列中塞请求(用一个类来模拟请求),任务队列不设上限。2、然后创建一批线程,然后线程不断往队列中拿数据,然后进行处理数据。当任务队列为空时,线程挂起等待。3、当服务器往队列中放入请求后,然后唤醒线程来处理请求。
//ThreadPool.hpp
#pragma once
#include<iostream>
#include<queue>
#include<pthread.h>
#include<unistd.h>
#define NUM 5
class Task{
public:
int base;
public:
Task()=default;
Task(int _base):base(_base){}
~Task(){}
void Run(){
std::cout<<"Thread id is ["<<pthread_self()<<"] base is #: "<<base<<" pow is $:"<<base*base<<std::endl;
}
};
class ThreadPool{
public:
std::queue<Task*> q; //放指针时不用初始化
pthread_mutex_t lock;
pthread_cond_t cond;//让消费者等,就是线程池中的线程
int max_thread;
static bool quit;
public:
void LockQueue(){
pthread_mutex_lock(&lock);
}
void UnLockQueue(){
pthread_mutex_unlock(&lock);
}
void ThreadWait(){
pthread_cond_wait(&cond,&lock);
}
bool IsEmpty(){
return q.empty();
}
void ThreadWakeUp(){
pthread_cond_signal(&cond);
}
void ThreadsWakeUp(){
pthread_cond_broadcast(&cond);
}
public:
ThreadPool(int max = NUM):max_thread(max){
}
static void* Routine(void *args){
ThreadPool *tp=(ThreadPool*)args;
pthread_detach(pthread_self());
while(!quit){
std::cout<<tp->q.size()<<std::endl;
tp->LockQueue();
while(!quit&&tp->IsEmpty()){
tp->ThreadWait();
}
Task t;
if(!quit&&!tp->IsEmpty()){
tp->Get(t);
tp->UnLockQueue();
t.Run();
}else{
tp->UnLockQueue();
}
}
}
void ThreadPoolInit(){
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&cond,nullptr);
pthread_t t;
for(int i=0;i<max_thread;i++){
pthread_create(&t,nullptr,Routine,this);
}
}
void Get(Task &out){
Task *t=q.front();
q.pop();
out=*t;
}
void Put(Task &in){
LockQueue();
q.push(&in);
UnLockQueue();
ThreadWakeUp();
}
~ThreadPool(){
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
void ThreadQuit(){
quit=true;
ThreadsWakeUp();
}
};
bool ThreadPool::quit=false;
//main.cpp
#include "ThreadPool.hpp"
int main(){
ThreadPool *tp=new ThreadPool();
tp->ThreadPoolInit();
int count=20;
while(count){
int x=rand()%10+1;
Task t(x);
tp->Put(t);
count--;
}
tp->ThreadQuit();
delete tp;
return 0;
}
//Makefile
mybin:main.cpp
g++ $^ -o $@ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf mybin