【Linux多线程】生产者消费者模型
目录
- 【Linux多线程】生产者消费者模型
- 生产者消费者模型
- 为何要使用生产者消费者模型
- 生产者消费者的三种关系
- 生产者消费者模型优点
- 基于BlockingQueue的生产者消费者模型
- C++ queue模拟阻塞队列的生产消费模型
- 伪唤醒情况(多生产多消费的情况下)
作者:爱写代码的刚子
时间:2024.3.29
前言:本篇博客将会介绍Linux多线程中一个非常重要的模型——生产者消费者模型
生产者消费者模型
- 321原则(方便记忆):3种关系,2种角色(生产者和消费者),1个交易场所(特定结构的内存空间)
为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
生产者消费者的三种关系
- 生产者VS生产者 :互斥
- 消费者VS消费者:互斥
- 生产者VS消费者:互斥,同步
生产者消费者模型优点
- 生产和消费进行解耦(多线程其实也是一种解耦)
- 支持并发
- 支持忙闲不均
基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构(有点类似于管道)。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
C++ queue模拟阻塞队列的生产消费模型
代码:
以下代码以单生产者,单消费者为例:
- 代码一:
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
template <class T>
class BlockQueue
{
static const int defalutnum = 5;
public:
BlockQueue(int maxcap=defalutnum):maxcap_(maxcap)
{
pthread_mutex_init(&mutex_,nullptr);
pthread_cond_init(&c_cond_,nullptr);
pthread_cond_init(&p_cond_,nullptr);
}
T pop()
{
pthread_mutex_lock(&mutex_);
if(q_.size()== 0 )
{
pthread_cond_wait(&c_cond_,&mutex_);//生产和消费要使用不同的等待队列
}
T out = q_.front();
q_.pop();
pthread_cond_signal(&p_cond_);
pthread_mutex_unlock(&mutex_);
return out;
}
void push(const T &in)
{
pthread_mutex_lock(&mutex_);
if(q_.size()==maxcap_)//判断本身也是访问临界资源
{
pthread_cond_wait(&p_cond_,&mutex_);//调度时自动释放锁
}
//1.队列没满 2.被唤醒
q_.push(in);
pthread_cond_signal(&c_cond_);
pthread_mutex_unlock(&mutex_);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&c_cond_);
pthread_cond_destroy(&p_cond_);
}
private:
std::queue<T> q_;//我们不直接使用stl中的queue是因为它本身不是线程安全的,共享资源
//int mincap_;
int maxcap_;//队列中的极值
pthread_mutex_t mutex_;
pthread_cond_t c_cond_;
pthread_cond_t p_cond_;
};
#include "BlockQueue.hpp"
#include <unistd.h>
void *Consumer(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
while(true)
{
sleep(2);// 由于两个线程谁先执行是不确定的,我们让生产者先执行
int data = bq->pop();
std::cout<<"消费了一个数据: "<<data<<std::endl;
}
}
void *Productor(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
int data=0;
while(true)
{
++data;
bq->push(data);
std::cout<<"生产了一个数据: "<<data<<std::endl;
}
}
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>();
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;
}
- 调整代码,使其生产者生产的数据到达一定范围通知消费者,消费者消费了一定的数据通知生产者:
【问】:生产者的数据从哪里来?
用户,或者网络等。生产者生产的数据也是要花时间获取的!,所以生产者要做两件事:1. 获取数据 2. 生产数据到队列
- 同时消费者拿到数据要做加工处理,也要花时间!,消费者要做两件事:1. 消费数据 2. 加工处理数据
【问】:生产者消费者模型为什么是高效的?
存在一个线程访问临界区的代码,一个线程正在处理数据,高效并发。
虽然互斥和同步谈不上高效,更何况加了锁,但是一个线程正在生产数据,一个线程正在消费数据,两者解偶且互不影响。(在更多的生产者消费者情况下,只有少量的执行流在互斥和同步,而大量的执行流都在并发访问)
- 再次完善代码,使该生产者消费者模型能够执行相应的任务:
BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
template <class T>
class BlockQueue
{
static const int defalutnum = 20;
public:
BlockQueue(int maxcap=defalutnum):maxcap_(maxcap)
{
pthread_mutex_init(&mutex_,nullptr);
pthread_cond_init(&c_cond_,nullptr);
pthread_cond_init(&p_cond_,nullptr);
low_water_ =maxcap_/3;
high_water_ =(maxcap_*2)/3;
}
T pop()
{
pthread_mutex_lock(&mutex_);
if(q_.size()== 0 )
{
pthread_cond_wait(&c_cond_,&mutex_);//生产和消费要使用不同的等待队列
}
T out = q_.front();
q_.pop();
if(q_.size()<low_water_)
{
pthread_cond_signal(&p_cond_);
}
pthread_mutex_unlock(&mutex_);
return out;
}
void push(const T &in)
{
pthread_mutex_lock(&mutex_);
if(q_.size()==maxcap_)//判断本身也是访问临界资源
{
pthread_cond_wait(&p_cond_,&mutex_);//调度时自动释放锁
}
//1.队列没满 2.被唤醒
q_.push(in);
if(q_.size()>high_water_)
{
pthread_cond_signal(&c_cond_);
}
pthread_mutex_unlock(&mutex_);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&c_cond_);
pthread_cond_destroy(&p_cond_);
}
private:
std::queue<T> q_;//我们不直接使用stl中的queue是因为它本身不是线程安全的,共享资源
//int mincap_;
int maxcap_;//队列中的极值
pthread_mutex_t mutex_;
pthread_cond_t c_cond_;
pthread_cond_t p_cond_;
int high_water_;
int low_water_;
};
main.cc
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <unistd.h>
void *Consumer(void *args)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(args);
while(true)
{
Task t=bq->pop();
//t.run();
t();
std::cout<<"处理任务: "<<t.GetTask()<<" 运算结果是: "<<t.GetResult()<<std::endl;
//sleep(2);// 由于两个线程谁先执行是不确定的,我们让生产者先执行
//std::cout<<"消费了一个数据: "<<data<<std::endl;
}
}
void *Productor(void *args)
{
int len = opers.size();
BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(args);
int data=0;
while(true)
{
int data1=rand()%10+1;
usleep(10);
int data2=rand() % 10;
char op =opers[rand() % len];
Task t(data1,data2,op);
//++data;
bq->push(t);
//std::cout<<"生产了一个数据: "<<data<<std::endl;
std::cout<<"生产了一个任务:"<< t.GetTask() <<std::endl;
sleep(1);
}
}
int main()
{
srand(time(nullptr));
BlockQueue<Task> *bq = new BlockQueue<Task>();
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;
}
Task.hpp
#pragma once
#include <iostream>
#include <string>
std::string opers = "+-*/%";
enum
{
DivZero = 1,
ModZero,
Unknown
};
class Task
{
public:
Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0)
{
}
void run()
{
switch (oper_)
{
case '+':
result_ = data1_ + data2_;
break;
case '-':
result_ = data1_ - data2_;
break;
case '*':
result_ = data1_ * data2_;
break;
case '/':
{
if (data2_ == 0)
exitcode_ = DivZero;
else
result_ = data1_ / data2_;
}
break;
case '%':
{
if (data2_ == 0)
exitcode_ = ModZero;
else
result_ = data1_ % data2_;
}
break;
default:
exitcode_ = Unknown;
break;
}
}
void operator ()()
{
run();
}
std::string GetResult()
{
std::string r = std::to_string(data1_);
r += oper_;
r += std::to_string(data2_);
r += "=";
r += std::to_string(result_);
r += "[code: ";
r += std::to_string(exitcode_);
r += "]";
return r;
}
std::string GetTask()
{
std::string r = std::to_string(data1_);
r+=oper_;
r += std::to_string(data2_);
r += "=?";
return r;
}
~Task()
{}
private:
int data1_;
int data2_;
char oper_;
int result_;
int exitcode_;
};
一定要记得,判断临界资源是否满足,也是在访问临界资源!!!
伪唤醒情况(多生产多消费的情况下)
多生产多消费的情况下:
举例:生产者只生产了一个数据,但是唤醒了多个消费者,多个消费者都在等待队列上,生产者将锁解开,多个消费者竞争这一把锁,其中一个消费者抢到了这把锁消费了一个数据,把锁解开,同时其他刚被唤醒的消费者其中又抢到了锁,进行消费,可是已经没有数据了(条件并不满足了),造成了伪唤醒的情况。(处于等待队列中的线程申请锁失败了会继续在条件变量中的等待队列中等)
或者说可能存在等待失败但是继续向下走的情况。
如何防止线程出现这种情况?
将if改成while(进行重复判断):
【问题】:无论是多生产多消费还是单生产单消费,本质上都是一个线程访问临界资源,那意义在哪?
重点是并发生产,并发消费,只是访问临界资源时是单个线程。重点不是获取数据本身,而在于处理数据!!!(本质)