生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过这个容器来通讯,所以生产者生产完数据之后不用等待消费者处理,直接将生产的数据放到这个容器当中,消费者也不用找生产者要数据,而是直接从这个容器里取数据,这个容器就相当于一个缓冲区,平衡了生产者和消费者的处理能力,这个容器实际上就是用来给生产者和消费者解耦的。
生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下:
我们用代码编写生产者消费者模型的时候,本质就是对这三个特点进行维护。
生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来。其中,所有的生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系。
注意: 互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来。
如果我们在主函数中调用某一函数,那么我们必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合。
对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合。
在多线程编程中,**阻塞队列(Blocking Queue)**是一种常用于实现生产者和消费者模型的数据结构。
其与普通的队列的区别在于:
为了方便理解,下面我们以单生产者、单消费者为例进行实现。
代码演示:
BlocQueue.hpp:
#include
#include
#include
#include
#define NUM 5
template<class T>
class BlockQueue
{
private:
std::queue<T> _q; // 阻塞队列
int _capcatity; // 最大容量
pthread_mutex_t _mutex; // 锁
pthread_cond_t _full; // 条件变量判断满
pthread_cond_t _empty; // 条件变量判断空
private:
bool IsFull()
{
return _q.size() == _capcatity;
}
bool IsEmpty()
{
return _q.empty();
}
public:
// 构造函数
BlockQueue(int cap = NUM)
:_capcatity(cap)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_full, NULL);
pthread_cond_init(&_empty, NULL);
}
// 析构函数
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_full);
pthread_cond_destroy(&_empty);
}
// 生产者调用向阻塞队列中插入数据
void Push(const T& data)
{
// 1、先上个锁
pthread_mutex_lock(&_mutex);
// 2、判是否满
while(IsFull())
{
// 满了则等待
pthread_cond_wait(&_full, &_mutex);
}
// 3、没满则插入
_q.push(data);
// 4、解锁
pthread_mutex_unlock(&_mutex);
// 5、唤醒在empty下的条件变量
pthread_cond_signal(&_empty);
}
// 消费者调用向阻塞队列中拿取数据
void Pop(T& data)
{
// 1、先上个锁
pthread_mutex_lock(&_mutex);
// 2、判断是否为空
while(IsEmpty())
{
// 空了则等待
pthread_cond_wait(&_empty, &_mutex);
}
// 3、拿取数据
data = _q.front();
_q.pop();
// 4、解锁
pthread_mutex_unlock(&_mutex);
// 5、唤醒在full下的条件变量
pthread_cond_signal(&_full);
}
};
说明:
判断是否满足生产消费条件时不能用if,而应该用while:
在主函数中我们就只需要创建一个生产者线程和一个消费者线程,让生产者线程不断生产数据,让消费者线程不断消费数据。
main.cc:
#include"BlockQueue.hpp"
void* Product(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 生产者进行生产数据
while(1)
{
sleep(1);
int data = rand() % 100 + 1;
bq->Push(data);
std::cout << "Productor:" << data << std::endl;
}
}
void* Consume(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 消费者进行消费数据
while(1)
{
sleep(1);
int data = 0;
bq->Pop(data);
std::cout << "Consumer:" << data << std::endl;
}
}
int main()
{
srand((unsigned int)time(nullptr));
// 消费者和生产者
pthread_t productor, consumer;
BlockQueue<int>* bq = new BlockQueue<int>;
// 创造
pthread_create(&productor, nullptr, Product, bq);
pthread_create(&consumer, nullptr, Consume, bq);
// 等待
pthread_join(productor, nullptr);
pthread_join(consumer, nullptr);
delete bq; // 防止内存泄露
return 0;
}
说明:
我们上述的代码就是消费者和生产者步调一致,生产者生产一个消费者消费一个,我们直接./来进行查看:
注意:以.hpp为后缀的文件也是头文件,该头文件同时包含类的定义与实现,调用者只需include该hpp文件即可。因为开源项目一般不需要进行保护,所以在开源项目中用的比较多。
void* Product(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 生产者进行生产数据
while(1)
{
int data = rand() % 100 + 1;
bq->Push(data);
std::cout << "Productor:" << data << std::endl;
}
}
void* Consume(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 消费者进行消费数据
while(1)
{
sleep(1);
int data = 0;
bq->Pop(data);
std::cout << "Consumer:" << data << std::endl;
}
}
此时由于生产者生产的很快,运行代码后一瞬间生产者就将阻塞队列打满了,此时生产者想要再进行生产就只能在full条件变量下进行等待,直到消费者消费完一个数据后,生产者才会被唤醒进而继续进行生产,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。
void* Product(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 生产者进行生产数据
while(1)
{
sleep(1);
int data = rand() % 100 + 1;
bq->Push(data);
std::cout << "Productor:" << data << std::endl;
}
}
void* Consume(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 消费者进行消费数据
while(1)
{
int data = 0;
bq->Pop(data);
std::cout << "Consumer:" << data << std::endl;
}
}
虽然消费者消费的很快,但一开始阻塞队列中是没有数据的,因此消费者只能在empty条件变量下进行等待,直到生产者生产完一个数据后,消费者才会被唤醒进而进行消费,消费者消费完这一个数据后又会进行等待,因此生产者和消费者的步调就是一致的。
我们也可以当阻塞队列当中存储的数据大于队列容量的三分之一时,再唤醒消费者线程进行消费;当阻塞队列当中存储的数据小于队列容器的三分之一时,再唤醒生产者线程进行生产。
// BlockQueue.hpp
// 生产者调用向阻塞队列中插入数据
void Push(const T& data)
{
// 1、先上个锁
pthread_mutex_lock(&_mutex);
// 2、判是否满
while(IsFull())
{
// 满了则等待
pthread_cond_wait(&_full, &_mutex);
}
// 3、没满则插入
_q.push(data);
// 4、唤醒在empty下的条件变量
if(_q.size() >= _capcatity / 3)
pthread_cond_signal(&_empty);
// 5、解锁
pthread_mutex_unlock(&_mutex);
}
// 消费者调用向阻塞队列中拿取数据
void Pop(T& data)
{
// 1、先上个锁
pthread_mutex_lock(&_mutex);
// 2、判断是否为空
while(IsEmpty())
{
// 空了则等待
pthread_cond_wait(&_empty, &_mutex);
}
// 3、拿取数据
data = _q.front();
_q.pop();
// 4、唤醒在full下的条件变量
if(_q.size() <= _capcatity / 3)
pthread_cond_signal(&_full);
// 5、解锁
pthread_mutex_unlock(&_mutex);
}
我们仍然让生产者生产的快,消费者消费的慢。运行代码后生产者还是一瞬间将阻塞队列打满后进行等待,但此时不是消费者消费一个数据就唤醒生产者线程,而是当阻塞队列当中的数据小于队列容器的一半时,才会唤醒生产者线程进行生产。
// main.cc
void* Product(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 生产者进行生产数据
while(1)
{
int data = rand() % 100 + 1;
bq->Push(data);
std::cout << "Productor:" << data << std::endl;
}
}
void* Consume(void* arc)
{
BlockQueue<int>* bq = (BlockQueue<int>*)arc;
// 消费者进行消费数据
while(1)
{
sleep(1);
int data = 0;
bq->Pop(data);
std::cout << "Consumer:" << data << std::endl;
}
}
我们想要实现一个基于计算任务的生产者消费者模型,此时我们只需要定义一个Task类,这个类当中需要包含一个Run成员函数,该函数代表着我们想让消费者如何处理拿到的数据。
BlockQueue.hpp:
#include
#include
#include
#include
#define NUM 5
template<class T>
class BlockQueue
{
private:
std::queue<T> _q; // 阻塞队列
int _capcatity; // 最大容量
pthread_mutex_t _mutex; // 锁
pthread_cond_t _full; // 条件变量判断满
pthread_cond_t _empty; // 条件变量判断空
private:
bool IsFull()
{
return _q.size() == _capcatity;
}
bool IsEmpty()
{
return _q.empty();
}
public:
// 构造函数
BlockQueue(int cap = NUM)
:_capcatity(cap)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_full, NULL);
pthread_cond_init(&_empty, NULL);
}
// 析构函数
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_full);
pthread_cond_destroy(&_empty);
}
// 生产者调用向阻塞队列中插入数据
void Push(const T& data)
{
// 1、先上个锁
pthread_mutex_lock(&_mutex);
// 2、判是否满
while(IsFull())
{
// 满了则等待
pthread_cond_wait(&_full, &_mutex);
}
// 3、没满则插入
_q.push(data);
// 4、解锁
pthread_mutex_unlock(&_mutex);
// 5、唤醒在empty下的条件变量
pthread_cond_signal(&_empty);
}
// 消费者调用向阻塞队列中拿取数据
void Pop(T& data)
{
// 1、先上个锁
pthread_mutex_lock(&_mutex);
// 2、判断是否为空
while(IsEmpty())
{
// 空了则等待
pthread_cond_wait(&_empty, &_mutex);
}
// 3、拿取数据
data = _q.front();
_q.pop();
// 4、解锁
pthread_mutex_unlock(&_mutex);
// 5、唤醒在full下的条件变量
pthread_cond_signal(&_full);
}
};
class Task
{
public:
Task(int x = 0, int y = 0, int op = 0)
: _x(x), _y(y), _op(op)
{}
~Task()
{}
void Run()
{
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::cout << "Warning: div zero!" << std::endl;
result = -1;
}
else{
result = _x / _y;
}
break;
case '%':
if (_y == 0){
std::cout << "Warning: mod zero!" << std::endl;
result = -1;
}
else{
result = _x % _y;
}
break;
default:
std::cout << "error operation!" << std::endl;
break;
}
std::cout << _x << _op << _y << "=" << result << std::endl;
}
private:
int _x;
int _y;
char _op;
};
main.cc:
#include"BlockQueue.hpp"
void* Product(void* arg)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
const char* arr = "+-*/%";
//生产者不断进行生产
while (1)
{
//sleep(1);
int x = rand() % 100 + 1;
int y = rand() % 100 + 1;
char op = arr[rand() % 5];
Task t(x, y, op);
bq->Push(t); //生产数据
std::cout << "producer task done" << std::endl;
}
}
void* Consume(void* arg)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
//消费者不断进行消费
while (1)
{
sleep(1);
Task t;
bq->Pop(t);
t.Run();
}
}
int main()
{
//srand((unsigned int)time(nullptr));
// 消费者和生产者
pthread_t productor, consumer;
BlockQueue<Task>* bq = new BlockQueue<Task>;
// 创造
pthread_create(&productor, nullptr, Product, bq);
pthread_create(&consumer, nullptr, Consume, bq);
// 等待
pthread_join(productor, nullptr);
pthread_join(consumer, nullptr);
delete bq; // 防止内存泄露
return 0;
}
运行代码,当阻塞队列被生产者打满后消费者被唤醒,此时消费者在消费数据时执行的就是计算任务,当阻塞队列当中的数据被消费到低于一定阈值后又会唤醒生产者进行生产。此后我们想让生产者消费者模型处理某一种任务时,就只需要提供对应的Task类,然后让该Task类提供一个对应的Run成员函数告诉我们应该如何处理这个任务即可。