生产者与消费者模型
什么是生产者消费者模型
- 某个模块负责产生数据,这些数据由另一个模块来负责处理,产生数据的模块,形象地称为生产者,而处理数据的模块,就称为消费者,该模式还需有一个缓冲区处于生产者和消费者之间,作为一个中介,生产者把数据放入缓冲区,而消费者从缓冲区取出数据。在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区 别在于:当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存 放元素的操作也会被阻塞,直到有元素被从队列中取出
为什么要使用生产者消费者模型
- 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯, 而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生 产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个 阻塞队列就是用来给生产者和消费者解耦的。
生产者与消费者之间的关系
-
1.三种关系:生产者与生产者(互斥)、消费者与消费者(互斥)、生产者与消费者(互斥、同步)
-
2.两种角色:生产者 、消费者
-
3.一种场所:为生产者与消费者提供数据单元的缓冲区
为了方便记忆:321原则
生产者与消费者模型的优点
- 1、解耦合:如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合),将来如果消费者的代码发生变化,可能会影响到生产者,而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低;
- 2、支持并发:当有多个生产者和多个消费者的时候,这个队列如果是一个线程安全的队列,意味着可以有多个生产者的往里面放数据,多个消费者从里面取数据。
- 3、支持忙闲不均:若制造数据的速度时快时慢,缓冲区就有优势,当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中,等生产者的制造速度慢下来,消费者再慢慢消费。
以阻塞队列为容器实现的生产者消费者模型
#include<iostream>
#include<queue>
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#define MAX_NUM 10
class BlockQueue
{
private:
std::queue<int> _queue;
int _capacity;
pthread_cond_t _cond_pro;
pthread_cond_t _cond_con;
pthread_mutex_t mutex;
public:
BlockQueue(int que_Maxcapacity)
:_capacity(que_Maxcapacity)
{
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&_cond_pro,NULL);
pthread_cond_init(&_cond_con,NULL);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&_cond_pro);
pthread_cond_destroy(&_cond_con);
}
//提供给生产者的接口(数据入队)
bool queuePush(int& data)
{
//queue是一个临界资源所以需要加锁保护
pthread_mutex_lock(&mutex);
//判断队列是否添加满了
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_pro,&mutex);
}
_queue.push(data);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&_cond_con);
return true;
}
//提供给消费者的接口(数据出队)
bool queuePop(int& data)
{
pthread_mutex_lock(&mutex);
//判断队列是否为空
while(_queue.empty()){
pthread_cond_wait(&_cond_con,&mutex);
}
data = _queue.front();
_queue.pop();
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&_cond_pro);
return true;
}
};
void* pro_thr(void* arg)
{
int i = 0;
BlockQueue* queue = (BlockQueue*)arg;
while(1){
queue->queuePush(i);
printf("pro_thr push %d\n",i++);
}
return NULL;
}
void* con_thr(void* arg)
{
BlockQueue* queue = (BlockQueue*)arg;
while(1){
int data;
queue->queuePop(data);
printf("con_thr get %d\n",data);
}
return NULL;
}
#define MAX_THR 5
int main()
{
BlockQueue queue(MAX_NUM);
pthread_t pro_tid[MAX_THR];
pthread_t con_tid[MAX_THR];
int i = 0;
int ret = 0;
for(i = 0 ; i < MAX_THR; i++)
{
pthread_create(&pro_tid[i],NULL,pro_thr,(void*)&queue);
if(ret != 0)
{
std::cerr << "pthread_create pro_thr error\n";
return -1;
}
}
for(i = 0; i < MAX_THR; i++)
{
pthread_create(&con_tid[i],NULL,con_thr,(void*)&queue);
if(ret != 0)
{
std::cerr << "pthread_create pro_thr error\n";
return -1;
}
}
for(i = 0; i < MAX_THR; i++)
{
pthread_join(pro_tid[i],NULL);
pthread_join(con_tid[i],NULL);
}
return 0;
}
以环形队列(数组,读写指针)为容器实现的生产者消费者模型
class RingQueue
{
private:
std::vector<int> _queue;//使用数组实现环形队列
int _capacity;//初始化环形队列的结点数量
int _step_read;//当前读位置的数组下标
int _step_write;//当前写位置的数组下标
sem_t _sem_lock;//用于实现互斥的锁
sem_t _sem_con;//消费者等待队列,计数器对有数据的空间进行计数
sem_t _sem_pro;//生产者等待队列,计数器对空闲的空间进行计数
public:
RingQueue(int max_que = MAX_QUE)
:_capacity(max_que)
,_queue(max_que)
,_step_read(0)
,_step_write(0)
{
sem_init(&_sem_lock,0,1);
sem_init(&_sem_con,0,0);
sem_init(&_sem_pro,0,max_que);
}
~RingQueue()
{
sem_destroy(&_sem_lock);
sem_destroy(&_sem_con);
sem_destroy(&_sem_pro);
}
//一定要记得先判断是否能够访问临界资源再加锁,因为锁只保护临界资源
bool QueuePush(int &data)
{
//1、判断是否能够访问临界资源,判断空闲空间计数是否大于0
sem_wait(&_sem_pro);
//2、加锁
sem_wait(&_sem_lock);
//3、数据的入队操作
_queue[_step_write] = data;
_step_write = (_step_write + 1) % _capacity;
//4、解锁
sem_post(&_sem_lock);
//5、数据资源的空间计数+1,唤醒消费者
sem_post(&_sem_con);
return true;
}
bool QueuePop(int &data)
{
sem_wait(&_sem_con);
sem_wait(&_sem_lock);
data = _queue[_step_read];
_step_read = (_step_read + 1) % _capacity;
sem_post(&_sem_lock);
//5、空闲空间计数+1,唤醒生产者
sem_post(&_sem_pro);
return true;
}
};
void *productor(void* arg)
{
RingQueue* queue = (RingQueue*)arg;
int data = 0;
while(1){
queue->QueuePush(data);
printf("productor push a data : %d\n",data++);
}
return NULL;
}
void *consumer(void* arg)
{
RingQueue* queue = (RingQueue*)arg;
while(1){
int data = 0;
queue->QueuePop(data);
printf("consumer get a data : %d\n",data);
}
return NULL;
}
#define MAX_THR 4
int main()
{
pthread_t ctid[MAX_THR];
pthread_t ptid[MAX_THR];
RingQueue queue;
int i = 0;
int ret = 0;
for(i = 0; i < MAX_THR; i++)
{
ret = pthread_create(&ptid[i],NULL,consumer,(void*)&queue);
if (ret != 0)
{
std::cerr << "pthread_create consumer error\n";
return -1;
}
}
for(i = 0; i < MAX_THR; i++)
{
ret = pthread_create(&ctid[i],NULL,productor,(void*)&queue);
if (ret != 0)
{
std::cerr << "pthread_create productor error\n";
return -1;
}
}
for(i = 0 ; i < MAX_THR; i++)
{
pthread_join(ptid[i],NULL);
pthread_join(ctid[i],NULL);
}
return 0;
}
读者写者模型(了解就行)
什么是读写者模型
- 有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多,大量的读者线程只对临界资源进行读操作,由少量写者线程对临界资源进行修改。读者可以同时读,但是写者写的时候其他写者不能写、其他读者不能读。也就是读共享,写互斥。通过读写锁实现。
读写锁
-
普通的互斥锁实现不了读共享写互斥,所以为了实现读共享写互斥,所以就有了读写锁。读写锁的行为:
当前锁状态 读锁请求 写锁请求 无锁 可以 可以 读锁 可以 阻塞 写锁 阻塞 阻塞 -
如何实现读写锁
可以设置两个计数:一个读者计数(若要写,则保证读者计数为0)、一个写者计数(若要读,则要保证写者计数为0,若要写,也要保证写者计数为 0)。但是如果有大量读者一直读,则有可能造成写者饥饿,也就是一直无法加写锁。因此,读写锁还具有:读者优先和写者优先的优先级,在加锁的时候,拒绝后续其他的异类加锁防止出现饥饿情况。前面提到的互斥锁无法加锁时的等待是挂起等待,而且读写锁无法加锁时的等待是自旋等待,是通过自旋锁实现的。
-
自旋锁
自旋锁会循环一直判断条件是否满足,并不挂起线程,一直抢占这CPU资源,主要应用于明确条件的等待时间比较短的场景,否则会比较消耗CPU资源。 -
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁,当其他线程想要访问数据时,被阻塞挂起。
-
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据 前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。(CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若 不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。 )