**
生产者与消费者生产模型
**
这个是一种典型的设计模式–大佬们针对典型的应用场景设计的解决方案
为什么使用生产者与消费者模型?
解决大量的数据产出以及处理的问题。
通过一个容器解决生产者与消费者的强耦合问题(程序中的函数也是为了解决功能的强耦合问题),生产者与消费者使用一个阻塞队列来进行通讯,生产者产出数据直接扔给阻塞队列,消费者直接从阻塞队列中取,阻塞队列相当于缓冲区,平衡生产者消费者之间的处理能力。有线程不断生产数据,有线程不断处理数据。。
总结原则
1个场所
两个角色(生产者,消费者)
三种关系(生产者与生产者的互斥关系,消费者与消费者的互斥关系,生产正者与消费者的互斥与同步关系)
使用阻塞队列来处理生产者消费者的什么问题,以及有什么好处
1、解耦合
如果生产者与消费者在用一个执行流,那么只能生产一个解决一个,依赖性太强,功能耦合高
2、支持忙 闲 不均
在一边压力大一边压力小的时候,缓冲区队列缓冲大量数据,慢慢处理。
3、支持并发
支持多个消费者多个生产者,前提中间的缓冲区操作必须线程安全。
额外知识–并发并行理解
并发–轮询处理–多个执行流在cpu的调度下进行轮询处理。
并行–同时处理—在cpu条件足够的情况下,同时处理。
并行这个概念一般少说,除非cpu的资源非常多,可以支持并行。
有多线程的并发—操作系统层面的轮询调度(或者cpu资源足够多的情况下的并行)
后边网络编程讲到的多路转接模型–高并发服务器模型—应用层面的任务轮询处理
阻塞队列是一种经常实现生产者和消费者模型的数据结构。
针对于不同的线程来讲,与普通队列在于,
队列为空时,从队列拿元素会被阻塞,直到队列中放入了元素;
队列满了时,往队列放元素会被阻塞,直到队列中取出了元素。
形成自己的代码风格
输入型参数:一个数据传递给函数,函数内部使用就可以。const &
输出型参数:通过参数从函数内部获取数据的结果。从函数内部向外传输数据 int*
输入输出型参数:一个数据传递给函数使用并且传出数据处理的结果。int&
生产者消费者的实现–一个场所,两种角色,三种关系
对于生产者、消费者
其实只是两种业务处理的线程而已–我们创建线程就可以。
对于线程安全的队列–关键
装一个线程安全的BlockQueue–阻塞队列–向外提供线程安全的入队/出队操作
//阻塞队列的实现
#include<cstdio>
#include<iostream>
#include<queue>
#include<pthread.h>
#define QUEUE_MAX 5//queue的大小,用宏比较好
class BlockQueue
{
private:
//queue在BlockQueue类中,所以这个queue的类型是BlockQueue
//Blockqueue *queue;queue可以调用这个类中的所有接口pushpop等
std::queue<int> _queue;//STL的容器不是线程安全的,STL设计是奔着性能去的,这里要保证安全,所以要使用mutex.
int _capacity;//数据不能无限添加,内存耗尽程序就崩溃
pthread_mutex_t mutex; //互斥锁,为了保证容器安全
pthread_cond_t _productor_cond;//生产者队列,
pthread_cond_t _customer_cond;//消费者队列,大家各自拥有自己的队列,在唤醒的时候不容易冲突。
public:
//queue是std里边的不需要初始化,
//容量capacity肯定要初始化,capacity是一个栈上的变量,所以不用destory
//mutex,cond在学习时候就是必须要初始化的,并且用完还要进行destory.
BlockQueue(int max = QUEUE_MAX);//构造函数,参数给了一个缺省值
:capacity(max)
这个由于queue调用了push函数,在push的时候开空间,但是下边的vector是直接使用vector[_step_read]等等,所以要提前给其开空间
{
pthread_mutex_init(&_mutex,NULL);//在构造函数中尽量写能成功初始化的函数
pthread_cond_init(&_pro_cond,NULL);
pthread_cond_init(&_cus_cond,NULL);
}
~BlockQueue();//构造函数
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_pro_cond);
pthread_cond_destroy(&_cus_cond);
}
/入队是要访问queue的,它属于临界资源,是需要被保护的。
bool Push(const int& data);//入队,生产者将数据放在缓冲区队列中
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)//访问临界资源,首先加锁
{
//满了,则等待
pthread_cond_wait(&_pro_cond,&_mutex);//解锁 等待 被唤醒加锁一步完成
}
//1、被唤醒后加锁然后发现queue没满,2、或者第一次下来就发现queue没满
_queue.push(data);
pthread_mutex_unlock(&_mutex);//入完队就发现queue不访问了,解锁
pthread_cond_signal(&_cus_cond);
return true;//返回表示入队成功了
}
bool Pop(int *data);//出队,消费者拿到缓冲区的数据
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
pthread_cond_wait(&_cus_cond,&_mutex);
}
*data = _queue.front();//获取队首结点的数据
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_pro_cond);
return true;
}
//生产者的线程的操作
void *thr_productor(void* arg)
{
//线程需要操作queue,给队列进行入队,所以需要将queue当作线程入口函数的参数传递进来。
//但是在互斥锁实现以及厨师与顾客的实现中,线程的参数都是NULL,那是因为bowl和ticket都是全局变量。
//入口函数的参数类型是(void*),需要强转为BlockQueue*
Blockqueue *queue = (Blockqueue*)arg;
int i = 0;//数据
while(1)//因为创建了入口函数,进来了之后,需要不停的让线程push数据,但是线程只创建一次,push数据要不停,所以在线程函数这要用while(1)
{
queue->push(i);
printf("productor push data:%d\n",i++);
}
return NULL;//当线程被外界中断的时候,线程退出,反回
}
void *thr_customer(void* arg)
{
Blockqueue* queue = (Blockqueue*)arg;
while(1)
{
int data;//这块一般不容易想到,消费者要定义一个变量data去拿数据
queue->pop(&data);//Pop(int *data)这个data是拿数据
//在函数里边直接是*data = 队列头数据,就把数据拿出来了
printf("customer pop data:%d\n",data);
}
return NULL;
}
int main()
{
int ret,i;
pthread_t ptid[4],ctid[4];//创建4个线程生产数据,消费数据
因为在thr_productor和thr_customer函数中需要访问同一个队列,所以队列要定义在主函数中,
//因为queue这个临界资源是不安全的,需要mutex和cond,我们需要将其三者构造成一个类,类的实例化只能是局部变量,所以要把queue在创建线程时当作参数传递过去
//在创建线程的时候,通过(void*)&queue参数形式传递到函数,并且经过强转得到对象的地址,然后在线程函数中访问类队列的成员函数
Blockqueue queue(20);//创建一个类的对象,也是类型//20是队列的大小
//创建生产者线程
for(i = 0; i < 4; i++)
{
ret= pthread_create(&ptid[i],NULL,thr_productor,(void*)&queue);//注意这里传的是queue的地址,所以要&
if(ret != 0)
{
printf("creat productor thread error\n");
return -1;
}
}
//创建消费者线程
for(i = 0; i < 4; i++)
{
ret= pthread_create(&ptid[i],NULL,thr_customer,(void*)&queue);//注意这里传的是queue的地址,所以要&
if(ret != 0)
{
printf("creat customer thread error\n");
return -1;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(ptid[i],NULL);//线程要等待,回收资源,或者在入口函数那块detach线程。
pthread_join(ctid[i],NULL);
}
return 0;
116 }
**
这个程序可能会出现只有五个队列,但是会一次性get6个数据的情况,这是因为push和printf不是原子操作。
信号量POSIX标准
**
互斥锁的本质:
一个0/1的计数器,实现互斥
条件变量cond的本质是:
两个接口(wait,post/broadcast)+pcb等待队列,实现同步。
但是在厨师与顾客,生产者与消费者的模型中由于有bowl和queue的公共资源,所以还得使用mutex来实现。
信号量本质:一个计数器(可以记录很多资源)+pcb等待队列
信号量功能:实现进程/线程的同步与互斥(主要实现同步)
信号量同步的实现:–sem_t idle//sem_t data
通过计数器对资源进行计数,在 生产/获取资源 之前可以先通过计数得知访问的合理性。进行操作
p操作:访问资源之前访问信号量计数,访问合理,资源-1,不合理阻塞
v操作:生产资源之前访问信号量计数,访问合理,资源+1,唤醒一个阻塞的线程或者进程,不合理返回。
信号量互斥实现–sem_t lock:
只有一个计数,表示只有一个资源,同一时间只有一个线程或者进程可以访问。
0、定义信号量
sem_t sem;
1、初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
pshared:0表示线程间共享,1表示进程间共享。
value:信号量初始值,也是资源的计数。
2、访问临界资源之前进行p操作,让资源数-1—等待信号量
int sem_wait(sem_t *sem);通过自身计数判断是否满足访问条件,不满足则一直阻塞线程/进程
int sem_trywait(sem_t *sem);通过自身计数判断是否满足访问条件,不满足立即报错返回 ETIMEDOUT
int sem_timewait(sem_t *sem,timespec *timeout);不满足则等待指定时间,超时后报错返回-ETIMEDOUT
wait(互斥锁信号量),资源-1。相当于加锁
wait(空闲空间信号量)//可以访问,资源-1,不可以访问阻塞
wait(数据信号量)//可以访问,资源-1,不可以访问阻塞
3、生产资源之后进行v操作,让资源数+1,唤醒阻塞线程–发布信号量
int sem_post(sem_t *sem);
post(互斥锁信号量),资源+1。相当于解锁
post(空闲空间信号量)//可以访问,资源+1,不可以访问阻塞
post(数据信号量)//可以访问,资源+1,不可以访问阻塞
wait是一个让信号量-1的操作,访问哪个信号量,就让哪个信号量-1
post是一个让信号量+1的操作,访问哪个信号量,就让哪个信号量+1
4、销毁信号量
int sem_destroy(sem_t *sem);
上一个生产者消费者的例子是基于queue的,其空间可以动态分配,因为queue可以动态增长,
现在基于固定大小的环形队列重写这个程序(POSIX信号量):
基于环形队列的生产者消费者模型
class Ringqueue
{
//因为这个是资源一个一个+/-。需要迭代器指针来回跑,所以使用vector,也可以用list等等
private:
std::vector<int> _queue;//数组
int _capacity;//这是队列的容量
int _step_read;//获取数据位置下标
int _step_write;//写入数据位置下标
sem_t _lock;//这个信号量用于实现互斥,计数器=1+等待队列
sem_t _sem_idle;//这个信号量用于计数空闲空间,生产者访问这个信号量,初始值为结点的个数。
sem_t _sem_data;//这个信号量用于计数数据空间,消费者访问这个信号量。初始值为0.
public:
Ringqueue(int maxq = QUEUE_MAX)
:_capacity(maxq)
,_vector(maxq)//上边queue初始化没有这个
由于上边的queue调用了push函数,在push的时候开空间,但是这个vector是直接使用vector[_step_read]等等,所以要提前给其开空间
,_step_read(0)//这两个一定要进行初始化,否则会是随机值,导致运行的时候程序崩溃
,_step_write(0)//这两个一定要进行初始化,否则会是随机值,导致运行的时候程序崩溃,
//并且不管是放完数据,还是取完数据,_step_read,_step_write都是++,然后在空间上移动,只有资源数是在wait访问下-1,post访问下+1.
{
sem_init(&_lock,0,1);//互斥锁只需要一个资源
sem_init(&_sem_data,0,0);//数据刚开始的资源是0
sem_init(&_sem_idle,0,maxq);//空闲空闲的计数是max最大值
///上边的sem_data,sem_idle在操作的过程中,他们的第三个参数的技术值进行+/-
}
~Ringqueue()
{
sem_destroy(&_lock);
sem_destroy(&_sem_data);
sem_destroy(&_sem_idle);
}
bool push(int data)//入队数据应该是
{
//入队应该使用wait()先访问空闲的信号量
//可以访问,计数-1,继续向下走
//不能访问阻塞
sem_wait(&_sem_idle);
//只要走下来就是可以访问,访问临界资源要进行加锁
sem_wait(&_sem_lock);//加锁
//访问资源
_vector[_step_write] = data;
_step_write = (_step_write + 1)% _capacity;//走到最后,从头开始
//访问完资源,解锁
sem_post(&_lock);//唤醒加锁信号量,资源+1,相当于解锁
sem_post(&_sem_data); //给空间加了数据,就要去唤醒数据信号量,资源+1
return true;
}
bool pop(int *data)//出数据
{
//先访问一下有数据信号量
sem_wait(&sem_data);
//可以访问,data计数-1,然后进行访问资源,先进行加锁
//不可以访问则阻塞
sem_wait(&sem_lock);
*data = _vector[_read_step];
_step_read = (_step_read + 1) % _capacity;
sem_post(&_lock);//唤醒加锁信号量,资源+1,相当于解锁
sem_post(&_sem_idle); //给空间减了数据,就要去唤醒空闲信号量,资源+1
return true;
}
void* thr_productor(void* arg)
{
Ringqueue* vector = (Ringqueue*)arg;
int i = 0;//生产者生产的数据
while(1)
{
vector->push(i);
printf("productor push data:%d\n",i++);
}
return NULL;
}
void* thr_customer(void* arg)
{
Ringqueue* vector = (Ringqueue*)arg;
while(1)
{
int data;//这块消费者需要定义一个data变量去拿数据
vector->pop(&data);//这里边data的数据已经传地址拿到了,可以不用data接收,就这样就可以。
printf("customer pop data:%d\n",data);
}
return NULL;
}
int main()
{
int ret,i;
pthread_t ptid[4],ctid[4];//创建4个线程生产数据,消费数据
因为在thr_productor和thr_customer函数中需要访问同一个队列,所以队列要定义在主函数中,
//因为vector这个临界资源是不安全的,需要mutex和cond,我们需要将其三者构造成一个类,类的实例化只能是局部变量,所以要把queue在创建线程时当作参数传递过去
//在创建线程的时候,通过(void*)&queue参数形式传递到函数,并且经过强转得到对象的地址,然后在线程函数中访问类队列的成员函数
Blockqueue vector(20);//创建一个类的对象,也是类型//20是队列的大小
//创建生产者线程
for(i = 0; i < 4; i++)
{
ret= pthread_create(&ptid[i],NULL,thr_productor,(void*)&vector);//注意这里传的是queue的地址,所以要&
if(ret != 0)
{
printf("creat productor thread error\n");
return -1;
}
}
//创建消费者线程
for(i = 0; i < 4; i++)
{
ret= pthread_create(&ptid[i],NULL,thr_customer,(void*)&vector);//注意这里传的是queue的地址,所以要&
if(ret != 0)
{
printf("creat customer thread error\n");
return -1;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(ptid[i],NULL);//线程要等待,回收资源,或者在入口函数那块detach线程。
pthread_join(ctid[i],NULL);
}
return 0;
116 }
}
对于消费者生产者模型的阻塞任务队列类总结class BlockQueue{}
1、首先我们先定义一个阻塞的任务队列,第一个用queue是实现,第二个是一个环形的,使用vector来实现
2、由于queue和vector是为了性能而创建的,他们并不保证安全,第一个使用了mutex和cond_pro,cond_cus来实现任务队列安全和同步,第二个使用了信号量sem_t lock,sem_t idle,sem_t data,step_read,step_write来实现任务队列的安全和同步。
3、为了生产者生产的资源能够进入任务队列,为了消费者能够消耗资源,在任务队列里边还实现了push和pop。实现这两个函数的时候注意安全性。注意锁和条件变量的使用。
总结:所以在任务队列的类里边有私有成员变量,构造函数,析构函数,push,pop
对于主函数main和生产者消费者入口函数的总结
1、在main函数构造一个任务队列类的对象,然后创建消费者生产者的多个线程(消费者线程一个for循环,生产者线程一个for循环,然后线程等待一个for循环)。由于消费者生产者都得对任务队列进行操作,任务类对象又是局部变量,所以将任务类对象作为入口函数的参数传递过去。传递的时候注意类型的强转。
2、注意多个线程要么在入口函数detach或者在主函数main中join
3、在生产者消费者的入口函数中,利用传过去的任务队列类对象调用push传入资源,利用pop取出资源。这里线程应该是无限的生产消费,所以是while(1)
信号量的注意点:
信号量能实现同步(sem_t 空闲 多个资源数+等待队列)
与互斥(sem_t lock 一个资源数+等待队列)。
信号量与条件变量不同:
信号量通过自身的计数器来实现判断,不需要搭配互斥锁。
条件变量需要程序员自己进行判断,需要搭配互斥锁使用。
信号量与互斥锁的不同
信号量可以实现互斥,但更多实现的是同步。