目录
一、生产者消费者模型
1、生产者消费者模型的概念
2、生产者、消费者之间的关系
3、生产者和消费者的特点
二、基于BlockingQueue的生产者消费者模型(条件变量控制同步与互斥)
1、一个生产线程和一个消费线程完成的计算任务
1.1BlockQueue.hpp
1.2Task.hpp
1.3Main.cc
2、基于生产者消费者模型的生产、消费、存储的多线程代码
2.1BlockQueue.hpp
2.2Task.hpp
2.3Main.cc
3、基于生产者消费者模型的多生产者多消费者的多线程代码
4、生产者消费者模型真的高效吗?
三、基于环形队列的多生产者多消费者模型(信号量控制同步与互斥)
1、RingQueue.hpp
2、Task.hpp
3、main.cc
生产者消费者模型是通过一个中间容器来解决生产者和消费者之间的强耦合问题。生产者和消费者彼此之间不直接通讯,而是通过阻塞队列进行通讯。所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列相当于一个缓冲区。
三种生产者、消费者之间的关系:
两种角色:生产者线程和消费者线程
一个交易场所:一段特定结构的缓冲区
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。它与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出。
代码结果如下:
#pragma once
#include
#include
const int gMaxCap=5;
template
class BlockQueue
{
public:
BlockQueue(const int capacity=gMaxCap)
:_capacity(capacity)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_pcond,nullptr);
pthread_cond_init(&_ccond,nullptr);
}
void push(const T& in)//输入型参数:const &
{
pthread_mutex_lock(&_mutex);
//细节2:充当条件的判断必须是while,不能用if
//这是因为唤醒的时候存在唤醒异常或伪唤醒的情况
//需要让线程重新使用IsFull对空间就行判断,确保100%唤醒
while(IsFull())
{
//细节1:
//该线程被pthread_cond_wait函数挂起后,会自动释放锁。
//该线程被pthread_cond_signal函数唤醒后,会自动重新获取原来那把锁
pthread_cond_wait(&_pcond,&_mutex);//因为生产条件不满足,无法生产,此时我们的生产者进行等待
}
//走到这里一定没有满
_q.push(in);
//刚push了一个数据,可以试着消费者把他取出来(唤醒消费者)
//细节3:pthread_cond_signal()这个函数,可以放在临界区内部,也可以放在临界区外部
pthread_cond_signal(&_ccond);//可以设置水位线,满多少就唤醒消费者
pthread_mutex_unlock(&_mutex);
//pthread_cond_signal(&_ccond);//也可以放在解锁之后
}
void pop(T* out)//输出型参数:* //输入输出型:&
{
pthread_mutex_lock(&_mutex);
while(IsEmpty())
{
pthread_cond_wait(&_ccond,&_mutex);//消费者休眠
}
//先把数据处理好,再唤醒消费者
*out=_q.front();
_q.pop();
//走到这里,一定能保证不为空,唤醒生产者进行生产
pthread_cond_signal(&_pcond);
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_pcond);
pthread_cond_destroy(&_ccond);
}
private:
bool IsEmpty()
{
return _q.empty();
}
bool IsFull()
{
return _q.size()==_capacity;
}
private:
std::queue _q;
int _capacity;//阻塞队列的容量
pthread_mutex_t _mutex;//互斥锁
pthread_cond_t _pcond;//生产者对应的条件变量
pthread_cond_t _ccond;//消费者对应的条件变量
};
#pragma once
#include
#include
class Task
{
//using func=std::function;
typedef std::function func_t;//函数对象
public:
Task()
{}
Task(int x,int y,char op,func_t func)
:_x(x)
,_y(y)
,_op(op)
,_callBack(func)
{}
std::string operator()()
{
int result=_callBack(_x,_y,_op);
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%d %c %d=%d",_x,_op,_y,result);
return buffer;
}
std::string toTaskString()
{
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%d %c %d=?",_x,_op,_y);
return buffer;
}
private:
int _x;
int _y;
char _op;
func_t _callBack;//回调函数
};
#include
#include
#include
#include
#include
#include
#include "BlockQueue.hpp"
#include "Task.hpp"
const std::string oper="+-*/%";
int myMath(int x,int y,char op)
{
int result=0;
switch(op)
{
case '+':
result=x+y;
break;
case '-':
result=x-y;
break;
case '*':
result=x*y;
break;
case '/':
{
if(y==0)
{
std::cerr<<"div zero"<* bq=static_cast*>(args);
while(1)
{
//消费数据
Task t;
bq->pop(&t);
printf("Consumed task:%s\n",t().c_str());
//sleep(1);
}
return nullptr;
}
//生产者
void* productor(void* args)
{
BlockQueue* bq=static_cast*>(args);
while(1)
{
//生产数据
int x=rand()%10+1;
int y=rand()%5;
int operCode=rand()%oper.size();
Task t(x,y,oper[operCode],myMath);
bq->push(t);//把任务push进阻塞队列
printf("Production task:%s\n",t.toTaskString().c_str());
sleep(1);
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr)^getpid());
BlockQueue* bq=new BlockQueue();
pthread_t c,p;
pthread_create(&c,nullptr,consumer,bq);
pthread_create(&p,nullptr,productor,bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
delete bq;
return 0;
}
定义一个结构体BlockQueues用于封装计算任务的阻塞队列和存储任务的阻塞队列;
生产者线程执行productor函数域中的代码,用于生产运算任务对象CalTask t,并将其push进计算任务的阻塞队列中。
消费者线程执行consumer函数域中的代码,用于从任务队列中获取CalTask t,并通过CalTask类中的“仿函数”对结果进行计算,同时消费者线程还要将计算结果和Save方法生成一个SaveTask对象,将其存储于存储阻塞队列save_bq中;
存储线程执行saver函数域中的代码,通过拿到存储任务的阻塞队列save_bq,通过operator()间接调用Save方法,将数据结果追加式存储于文本文件中。
#pragma once
#include
#include
const int gMaxCap=5;
template
class BlockQueue
{
public:
BlockQueue(const int capacity=gMaxCap)
:_capacity(capacity)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_pcond,nullptr);
pthread_cond_init(&_ccond,nullptr);
}
void push(const T& in)//输入型参数:const &
{
pthread_mutex_lock(&_mutex);
//细节2:充当条件的判断必须是while,不能用if
//这是因为唤醒的时候存在唤醒异常或伪唤醒的情况
//需要让线程重新使用IsFull对空间就行判断,确保100%唤醒
while(IsFull())
{
//细节1:
//该线程被pthread_cond_wait函数挂起后,会自动释放锁。
//该线程被pthread_cond_signal函数唤醒后,会自动重新获取原来那把锁
pthread_cond_wait(&_pcond,&_mutex);//因为生产条件不满足,无法生产,此时我们的生产者进行等待
}
//走到这里一定没有满
_q.push(in);
//刚push了一个数据,可以试着消费者把他取出来(唤醒消费者)
//细节3:pthread_cond_signal()这个函数,可以放在临界区内部,也可以放在临界区外部
pthread_cond_signal(&_ccond);//可以设置水位线,满多少就唤醒消费者
pthread_mutex_unlock(&_mutex);
//pthread_cond_signal(&_ccond);//也可以放在解锁之后
}
void pop(T* out)//输出型参数:* //输入输出型:&
{
pthread_mutex_lock(&_mutex);
while(IsEmpty())
{
pthread_cond_wait(&_ccond,&_mutex);//消费者休眠
}
//先把数据处理好,再唤醒消费者
*out=_q.front();
_q.pop();
//走到这里,一定能保证不为空,唤醒生产者进行生产
pthread_cond_signal(&_pcond);
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_pcond);
pthread_cond_destroy(&_ccond);
}
private:
bool IsEmpty()
{
return _q.empty();
}
bool IsFull()
{
return _q.size()==_capacity;
}
private:
std::queue _q;
int _capacity;//阻塞队列的容量
pthread_mutex_t _mutex;//互斥锁
pthread_cond_t _pcond;//生产者对应的条件变量
pthread_cond_t _ccond;//消费者对应的条件变量
};
#pragma once
#include
#include
#include
class CalTask
{
//using func=std::function;
typedef std::function func_t;//函数对象
public:
CalTask()
{}
CalTask(int x,int y,char op,func_t func)
:_x(x)
,_y(y)
,_op(op)
,_callBack(func)
{}
std::string operator()()//消费者调用
{
int result=_callBack(_x,_y,_op);
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%d %c %d=%d",_x,_op,_y,result);//结果字符串
return buffer;
}
std::string toTaskString()//生产者调用
{
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%d %c %d=?",_x,_op,_y);
return buffer;
}
private:
int _x;
int _y;
char _op;//加减乘除取模
func_t _callBack;//回调函数
};
const std::string oper = "+-*/%";
int myMath(int x, int y, char op)
{
int result = 0;
switch (op)
{
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '*':
result = x * y;
break;
case '/':
{
if (y == 0)
{
std::cerr << "div zero error" << std::endl;
result = -1;
}
else
result = x / y;
}
break;
case '%':
{
if (y == 0)
{
std::cerr << "mod zero" << std::endl;
result = -1;
}
else
result = x % y;
}
break;
default:
break;
}
return result;
}
class SaveTask
{
typedef std::function func_t;
public:
SaveTask()
{}
SaveTask(const std::string& message,func_t func)
:_message(message)
,_func(func)
{}
void operator()()
{
_func(_message);//Main.cc传入的result
}
private:
std::string _message;
func_t _func;
};
void Save(const std::string& message)
{
const std::string target="./log.txt";
FILE* fp=fopen(target.c_str(),"a+");
if(fp==nullptr)
{
std::cerr<<"fopen error"<
#include
#include
#include
#include
#include
#include
#include "BlockQueue.hpp"
#include "Task.hpp"
//阻塞队列类。 C:计算任务,S:存储任务
template
struct BlockQueues
{
BlockQueue *c_bq;
BlockQueue *s_bq;
};
// 生产者跑这个函数,参与生产任务
void *productor(void *args)
{
BlockQueue* bq = (static_cast*>(args))->c_bq;//计算任务
while (1)
{
// 生产数据
int x = rand() % 10 + 1;
int y = rand() % 5;
int operCode = rand() % oper.size();
CalTask t(x, y, oper[operCode], myMath);
bq->push(t); // 把任务push进阻塞队列
printf("Productor thread,生产计算任务:%s\n", t.toTaskString().c_str());
sleep(1);
}
return nullptr;
}
// 消费者跑这个函数,参与计算任务和存储任务
void *consumer(void *args)
{
BlockQueue* bq = (static_cast*>(args))->c_bq;//计算任务
BlockQueue* save_bq = (static_cast*>(args))->s_bq;//存储任务
while (1)
{
// 消费数据,计算任务
CalTask t;
bq->pop(&t);
std::string result=t();
printf("Cal thread,完成计算任务:%s...done\n", result.c_str());
//存储任务
SaveTask save(result,Save);
save_bq->push(save);//把save对象push进储存阻塞队列中
printf("Cal thread,推送存储任务完成\n");
//sleep(1);
}
return nullptr;
}
//储存线程跑这个函数,参与存储任务
void* saver(void* args)
{
BlockQueue* save_bq = (static_cast*>(args))->s_bq;//拿到存储任务
while(1)
{
SaveTask t;
save_bq->pop(&t);
t();//调用消费者类中的Save方法
printf("save thread,保存任务完成...\n");
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid());
BlockQueues bqs;
// 两个阻塞队列
bqs.c_bq = new BlockQueue();
bqs.s_bq = new BlockQueue();
pthread_t c, p,s;//消费者、生产者、保存者线程
pthread_create(&p, nullptr, productor, &bqs);
pthread_create(&c, nullptr, consumer, &bqs);
pthread_create(&s, nullptr,saver ,&bqs);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
pthread_join(s, nullptr);
delete bqs.c_bq;
delete bqs.s_bq;
return 0;
}
#include
#include
#include
#include
#include
#include
#include "BlockQueue.hpp"
#include "Task.hpp"
//阻塞队列类。 C:计算任务,S:存储任务
template
struct BlockQueues
{
BlockQueue *c_bq;
BlockQueue *s_bq;
};
// 生产者跑这个函数,参与生产任务
void *productor(void *args)
{
BlockQueue* bq = (static_cast*>(args))->c_bq;//计算任务
while (1)
{
// 生产数据
int x = rand() % 10 + 1;
int y = rand() % 5;
int operCode = rand() % oper.size();
CalTask t(x, y, oper[operCode], myMath);
bq->push(t); // 把任务push进阻塞队列
printf("Productor thread,生产计算任务:%s\n", t.toTaskString().c_str());
sleep(1);
}
return nullptr;
}
// 消费者跑这个函数,参与计算任务和存储任务
void *consumer(void *args)
{
BlockQueue* bq = (static_cast*>(args))->c_bq;//计算任务
//BlockQueue* save_bq = (static_cast*>(args))->s_bq;//存储任务
while (1)
{
// 消费数据,计算任务
CalTask t;
bq->pop(&t);
std::string result=t();
printf("Cal thread,完成计算任务:%s...done\n", result.c_str());
}
return nullptr;
}
//储存线程跑这个函数,参与存储任务
void* saver(void* args)
{
BlockQueue* save_bq = (static_cast*>(args))->s_bq;//拿到存储任务
while(1)
{
SaveTask t;
save_bq->pop(&t);
t();//调用消费者类中的Save方法
printf("save thread,保存任务完成...\n");
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid());
BlockQueues bqs;
// 两个阻塞队列
bqs.c_bq = new BlockQueue();
bqs.s_bq = new BlockQueue();
pthread_t c[2], p[3],s;//消费者、生产者、保存者线程
pthread_create(c, nullptr, consumer, &bqs);
pthread_create(c+1, nullptr, consumer, &bqs);
pthread_create(p, nullptr, productor, &bqs);
pthread_create(p+1, nullptr, productor, &bqs);
pthread_create(p+2, nullptr, productor, &bqs);
//pthread_create(&s, nullptr,saver ,&bqs);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
//pthread_join(s, nullptr);
delete bqs.c_bq;
delete bqs.s_bq;
return 0;
}
对Main.cc进行改造,另外两个头文件不变。
该代码有多个生产者和消费者,通过BlockQueue.hpp中的BlockQueue类实例化出的对象中的push和pop方法,让各生产者和消费者共同去竞争同一把锁,保证在同一时间,只有一个线程抢到锁并执行对应的任务。
从本段第三节我们看到,大量的生产者、消费者全部在虎视眈眈的争夺同一把锁,也就是说,一次只能放一个线程去阻塞队列中完成任务,那效率不是非常慢?既然如此为何要采用这种模型?
因为传统的线程运作方式会让大部分线程阻塞在临界区之外,而生产者消费者模型则是将任务的工序拆开,一组线程分为生产者,另一组分为消费者。充分利用了生产者的阻塞时间,用以提前准备好生产资源;同时也利用了消费者计算耗时的问题,让消费者线程将更多的时间花在计算上,而不是抢不到锁造成线程“干等”。
生产者消费者模型可以在生产前和消费后,让线程并行执行,减少线程阻塞时间。
环形队列本质是一个数组模拟的首尾相连的队列。
在环形队列中,只有队列全空或者全满时,生产者和消费者才会站在队列的同一格里。当环形队列全空时,必须生产者线程先生产;当环形队列为满时,必须消费者线程先生产。在其他情况下,生产者和消费者线程可以并发执行,只有在队列为空或满的时候,才有同步与互斥问题。
我们可以给生产者定义一个信号量,用以表示剩余空间资源;给消费者定义一个信号量,用以表示数据资源。
void Push(const T& in)//向环形队列中push数据
{
P(_spaceSem);//调用sem_wait,信号量申请成功,未来一定能访问到临界资源
pthread_mutex_lock(&_pmutex);
_queue[_productorStep++]=in;
_productorStep%=_cap;
pthread_mutex_unlock(&_pmutex);
V(_dataSem);//生产完毕后,调用V操作,让消费者信号量++。
}
环形队列中最少有一个线程行动(为空或为满);最多有两个线程行动(生产消费各一个)。那么多生产者多消费者的意义是什么?
这个意义和上面讲的生产者消费者模型一样,线程在资源生产和消费的过程可能巨花时间,该方式可以让线程利用原先被阻塞的时间,用以生产和消费活动,提升总体效率。
#pragma once
#include
#include
#include
#include
#include
#include
static const int gcap=5;
template
class RingQueue
{
private:
void P(sem_t& spaceSem)
{
int n=sem_wait(&spaceSem);
assert(0==n);//这里最好if
(void)n;
}
void V(sem_t& dataSem)
{
int n=sem_post(&dataSem);
assert(0==n);//这里最好if
(void)n;
}
public:
RingQueue(const int& cap=gcap)
:_queue(cap)//可以这样初始化vector吗?
,_cap(cap)
,_productorStep(0)
,_consumerStep(0)
{
int n=sem_init(&_spaceSem,0,_cap);
assert(0==n);
n=sem_init(&_dataSem,0,0);
assert(0==n);
pthread_mutex_init(&_pmutex,nullptr);
pthread_mutex_init(&_cmutex,nullptr);
}
//生产者调用Push
void Push(const T& in)//向环形队列中push数据
{
P(_spaceSem);//调用sem_wait,信号量申请成功,未来一定能访问到临界资源
pthread_mutex_lock(&_pmutex);
_queue[_productorStep++]=in;
_productorStep%=_cap;
pthread_mutex_unlock(&_pmutex);
V(_dataSem);//生产完毕后,调用V操作,让消费者信号量++。
}
//消费者调用Pop
void Pop(T* out)//向环形队列中pop数据,out是输出型参数
{
P(_dataSem);//确认消费者信号量是否可以--,不能减就阻塞
pthread_mutex_lock(&_cmutex);
*out=_queue[_consumerStep++];
_consumerStep%=_cap;
pthread_mutex_unlock(&_cmutex);
V(_spaceSem);//消费完毕后,生产者信号量++
}
~RingQueue()
{
sem_destroy(&_spaceSem);
sem_destroy(&_dataSem);
pthread_mutex_destroy(&_pmutex);
pthread_mutex_destroy(&_cmutex);
}
private:
std::vector _queue;//vector模拟队列
int _cap;//队列的最大容量
sem_t _spaceSem;//生产者信号量,表明环形队列中剩余空间资源数量
sem_t _dataSem;//消费者信号量,表明环形队列中存在的数据资源数量
int _productorStep;//生产者在循环列表的脚步
int _consumerStep;//消费者在循环列表的脚步
pthread_mutex_t _pmutex;//生产者锁
pthread_mutex_t _cmutex;//消费者锁
};
#pragma once
#include
#include
#include
class Task
{
//using func=std::function;
typedef std::function func_t;//函数对象
public:
Task()
{}
Task(int x,int y,char op,func_t func)
:_x(x)
,_y(y)
,_op(op)
,_callBack(func)
{}
std::string operator()()//消费者调用
{
int result=_callBack(_x,_y,_op);
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%d %c %d=%d",_x,_op,_y,result);//结果字符串
return buffer;
}
std::string toTaskString()//生产者调用
{
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%d %c %d=?",_x,_op,_y);
return buffer;
}
private:
int _x;
int _y;
char _op;//加减乘除取模
func_t _callBack;//回调函数
};
const std::string oper = "+-*/%";
int myMath(int x, int y, char op)
{
int result = 0;
switch (op)
{
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '*':
result = x * y;
break;
case '/':
{
if (y == 0)
{
std::cerr << "div zero error" << std::endl;
result = -1;
}
else
result = x / y;
}
break;
case '%':
{
if (y == 0)
{
std::cerr << "mod zero" << std::endl;
result = -1;
}
else
result = x % y;
}
break;
default:
break;
}
return result;
}
#include
#include
#include
#include
#include
#include "RingQueue.hpp"
#include "Task.hpp"
std::string SelfName()
{
char name[128];
snprintf(name,sizeof(name),"thread:0X%x",pthread_self());
return name;
}
void* ProductorRoutine(void* product)
{
RingQueue* rqueue=static_cast*>(product);
while(1)
{
//模拟构建一个任务
int x=rand()%1000;
int y=rand()%2000;
char op=oper[rand()%oper.size()];
Task t(x,y,op,myMath);//构建任务对象
rqueue->Push(t);
std::cout<* rqueue=static_cast*>(consume);
while(1)
{
Task t;
rqueue->Pop(&t);
std::cout<* rq=new RingQueue();
pthread_t p[4],c[7];//定义生产者消费者线程
for(int i=0;i<4;++i)
pthread_create(p+i,nullptr,ProductorRoutine,rq);
for(int i=0;i<7;++i)
pthread_create(c+i,nullptr,ConsumerRoutine,rq);
for(int i=0;i<4;++i)
pthread_join(p[i],nullptr);
for(int i=0;i<7;++i)
pthread_join(c[i],nullptr);
return 0;
}