生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过这个容器来通讯,所以生产者生产完数据之后不用等待消费者处理,直接将生产的数据放到这个容器中,消费者也不用找生产者要数据,而是直接从这个容器里取数据,这个容器就相当于一个缓冲区,平衡了生产者和消费者的处理能力,这个容器实际上就是用来给生产者和消费者解耦的。
生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下;
我们用代码编写生产者消费者模型的时候,本质就是对这三个特点进行维护。
生产者和生产者,消费者和消费者,生产者和消费者,它们之间为什么存在互斥关系?
因为生产者和消费者之间的容器可能被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来。
其中,所有的生产者和消费者都会竞争式地申请锁,因此生产者和生产者,消费者和生产者,生产者和消费者之间都存在互斥关系。
生产者和消费者之间为什么会存在同步关系?
虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的。我们应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,再让消费者进行消费。
如果我们在主函数中调用某一函数,那么我们必须等该函数体执行完之后才继续执行主函数的后序代码,因此函数调用本质是一种紧耦合。
对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合。
在多线程编程中,阻塞队列(Block Queue)是一种常用于生产者和消费者模型的数据结构。
其与普通队列的区别在于:
看到以上阻塞队列的描述,我们很容易想到的就是管道,而阻塞队列最典型的应用场景实际上就是管道的实现。
模拟实现基于阻塞队列的生产者消费者模型
阻塞队列实现的生产者消费者模型的基本代码如下(单生产者单消费者):
#pragma once
#include
#include
#include
static const int gmaxcap = 5;
template <class T>
class BlockQueue
{
public:
BlockQueue(const int& maxcap = gmaxcap)
: _maxcap(maxcap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_pcond, nullptr);
pthread_cond_init(&_ccond, nullptr);
}
// 输入型参数,我们一般设置为const &
// 输出型参数,我们一般设置为 *
// 输入输出型,我们一般设置为 &
void push(const T& in)
{
pthread_mutex_lock(&_mutex);
while (is_full())
{
// 生产条件不满足,无法生产,此时我们的生产者进行等待
pthread_cond_wait(&_pcond, &_mutex);
}
_q.push(in);
pthread_cond_signal(&_ccond);
pthread_mutex_unlock(&_mutex);
}
void pop(T* out)
{
pthread_mutex_lock(&_mutex);
while (is_empty())
{
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(&_ccond);
pthread_cond_destroy(&_pcond);
}
private:
bool is_empty()
{
return _q.empty();
}
bool is_full()
{
return _q.size() == _maxcap;
}
private:
std::queue<T> _q;
int _maxcap; // 队列中元素的上限
pthread_mutex_t _mutex;
pthread_cond_t _pcond; // 生产者对应的条件变量
pthread_cond_t _ccond; // 消费者对应的条件变量
};
对于代码,有以下解释:
判断是否满足生产消费条件时不能用if,而应该用while:
主函数中的代码如下:
#include "BlockQueue.hpp"
#include
#include
#include
#include
void* consumer(void* bq_)
{
BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(bq_);
while (true)
{
// 消费活动
int data;
bq->pop(&data);
std::cout << "消费数据:" << data << std::endl;
}
return nullptr;
}
void* productor(void* bq_)
{
BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(bq_);
while (true)
{
// 生产活动
int data = rand() % 10 + 1;
bq->push(data);
std::cout << "生产数据:" << data << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
BlockQueue<int>* bq = new BlockQueue<int>();
pthread_t c, p;
// 传入bq,让两个线程看到同一份阻塞队列
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, productor, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete bq;
return 0;
}
当生产者生产很快,消费者消费很慢时,在生产者生生产的数据满了之后就会阻塞下来等待消费者,最终它们的步调相同。若生产者生产慢,消费者消费快也同理。
基于计算任务的生产者消费者模型
实际使用生产者消费者模型可不是简单地让生产者生产一个数据让消费者打印,我们这样做只是为了测试代码的正确性。
由于我们将BlockQueue中存储的数据进行了模板化,此时就可以让BlockQueue当中存储其他类型的数据。
例如,我们可以实现一个基于计算任务的生产消费模型,此时我们只需定义一个Task类,这个类当中需要包含一个Run成员,该函数代表着我们想让消费者如何处理拿到的数据。
#pragma once
#include
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;
};
主函数代码如下:
#include "BlockQueue.hpp"
#include "Task.hpp"
#include
#include
#include
#include
void* consumer(void* bq_)
{
BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(bq_);
while (true)
{
// 消费活动
Task t;
bq->pop(&t);
t.Run();
}
return nullptr;
}
void* productor(void* bq_)
{
BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(bq_);
const char* arr = "+-*/%";
while (true)
{
// 生产活动
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;
sleep(1);
}
return nullptr;
}
运行代码,此时消费者执行的就是计算任务了。