生产者消费者概念
上面是生产者消费者?
生产数据的一方称为生产者, 将数据拿走用于处理的称为消费者.
生产者将产生的数据放入一个固定的区域, 而消费者只需要从这个固定区域取出数据即可
这个存放数据的区域可以当作是超市.
厂家生产出产品后, 将产品送至超市. (厂家 == 生产者)
我们 (顾客), 则是去到超市购买我们需要的产品. (顾客 == 消费者)
那么生产者消费者模型有什么优点?
生产者和消费者之间通过共享缓冲区间接通信, 而不是直接调用对方的方法. 这种解耦使得生产者和消费者可以独立运行, 互不干扰, 降低了代码的复杂性和耦合性.
生产者只需要生产数据, 放入仓库中, 消费者只需要从仓库中取出数据即可. (降低耦合性)
生产者和消费者可以并行运行, 充分利用多核处理器的性能. 通过缓冲区, 消费者不必直接等待生产者生产数据, 然后进行处理, 只需要去仓库获取数据, 那么生产者生产数据时, 消费者也能同时消费数据. (提高效率)
生产者消费者实现
版本一
这里使用队列作为存储数据的仓库. 通过互斥锁, 条件变量来调节生产者与消费者之间的同步互斥.
template<class T>
class ProducerConsumerBlock
{
public:
ProducerConsumerBlock(int capacity = 5)
:_capacity(capacity),
_size(0)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_isempty, NULL);
pthread_cond_init(&_isfull, NULL);
}
// 生产者放入数据
void push(const T& data)
{
pthread_mutex_lock(&_mutex);
while(_size == _capacity) // 当仓库已经满了, 那么就让生产者加入休眠状态
{ // 使用 while 循环, 被唤醒后, 再次检查是否有空间, 安全性更高
pthread_cond_wait(&_isfull, &_mutex);
}
_qe.push(data); // 将数据放入队列中
++_size;
if(_size > _capacity / 2) // 当数据数量超过容量的一半, 就唤醒消费者
{
pthread_cond_broadcast(&_isempty); // 可以只唤醒一个, 也可以一次性全部唤醒
}
pthread_mutex_unlock(&_mutex);
}
void pop(T* out)
{
pthread_mutex_lock(&_mutex);
while(_size == 0) // 检测是否有数据, 没有数据那么就进行等待, 等待生产者唤醒消费者
{
pthread_cond_wait(&_isempty, &_mutex);
}
*out = _qe.front(); // 取出数据
_qe.pop();
--_size;
if(_size < _capacity / 2) // 当数据的数量小于容量的一半, 那么唤醒生产者
{
pthread_cond_broadcast(&_isfull);
}
pthread_mutex_unlock(&_mutex);
}
~ProducerConsumerBlock()
{}
private:
std::queue<T> _qe; // 存放数据的仓库
pthread_mutex_t _mutex; // 互斥访问仓库
int _size; // 当前已经有多少数据存放于仓库
int _capacity; // 仓库的最大容量
pthread_cond_t _isempty; // 仓库是否满了
pthread_cond_t _isfull; // 仓库是否空了
};
版本二
这个版本我们需要使用信号量来完成.
在上面的代码中, 我们通过一个互斥锁, 限制所有的生产者和消费者访问仓库. 但实际上, 生产者只关系是否还有空间, 消费者只关系是否还有数据, 它们之间获取的资源不相同, 所以直接用互斥锁来同时限制生产者和消费者是效率比较低的. 这里可以使用信号量.
信号量: 本质上就是一个计数器, 可以用来记录资源的数量. 当线程需要什么资源时, 就需要先将这个计数器减一 (计数器不能小于 0), 如果减一成功, 那么获取资源就成功了. 如减一不成功, 那么就也会进行等待, 直到减一成功 (申请资源成功).
sem: _space(5) 剩余空间资源, 5 表示初始化大小为 5, 即刚开始空间资源为 5 份
sem: _data(0) 剩余数据资源, 0 表示初始化大小0, 刚开始数据资源为 0 .
生产者申请 _space, 申请成功, 那么生产者就继续向下执行, 否则进行等待
消费者则申请 _data, 申请成功, 那么消费者就继续向下执行, 否则进行等待
信号量的使用
1. 初始化信号量
使用函数: int sem_init(sem_t* sem, int pshared, unsigned int value);
pshared: 0 表示线程间共享, 非零表示进程间共享
value: 信号量初始值
sem_t _space_sem;
sem_init(&_space_sem, 0, 5);
2. 申请资源
使用函数: int sem_wait(sem_t* sem)
使用之后, sem 的值就会减一 (不能小于 0)
sem_wait(&_space_sem);
3. 释放资源
使用函数: int sem_post(sem_t* sem)
使用之后, sem 的值就会加一
sem_post(&_space_sem);
4. 销毁信号量
使用函数: int sem_destroy(semt_t* sem)
sem_destroy(&_space_sem);
总的来说信号量是比较简单的. 用法上和条件变量是差不多的, 只不过条件变量大小固定就是1, 而信号量的大小可以由自己控制.
代码实现
这里用于存放数据的仓库不再使用队列, 这里使用环形队列.
template<class T>
class ProducerConsumer
{
public:
ProducerConsumer(int n = 5)
:_data(n),
_push_index(0),
_pop_index(0),
_size(0)
{
pthread_mutex_init(&_producer, NULL);
pthread_mutex_init(&_consumer, NULL);
sem_init(&_space_sem, 0, n);
sem_init(&_data_sem, 0, 0);
}
void push(const T& data)
{
sem_wait(&_space_sem); // 申请空间资源
pthread_mutex_lock(&_producer); // 申请资源成功, 对仓库进行加锁, 放入数据
_data[_push_index] = data;
_push_index = (_push_index + 1) % _data.size();
_size++;
pthread_mutex_unlock(&_producer);
sem_post(&_data_sem); // 增加数据资源
}
void pop(T* out)
{
sem_wait(&_data_sem); // 申请数据资源
pthread_mutex_lock(&_consumer); // 获取数据
*out = _data[_pop_index];
_pop_index = (_pop_index + 1) % _data.size();
_size--;
pthread_mutex_unlock(&_consumer);
sem_post(&_space_sem); // 增加空间资源
}
~ProducerConsumer()
{
pthread_mutex_destroy(&_producer);
pthread_mutex_destroy(&_consumer);
sem_destroy(&_space_sem);
sem_destroy(&_data_sem);
}
private:
std::vector<T> _data; // 存放数据的仓库
int _push_index; // 放入数据的位置 (下标)
int _pop_index; // 取出哪个位置的数据 (下标)
int _size; // 当前已经存放了多少数据
sem_t _space_sem; // 代表剩余空间资源的信号量
sem_t _data_sem; // 代表剩余数据资源的信号量
pthread_mutex_t _producer; // 生产者之间的互斥
pthread_mutex_t _consumer; // 消费者之间的互斥
};