Linux:基于环形队列的生产者消费模型
文章目录
- 一、POSIX 信号量简介
- 二、POSIX 信号量的操作
- 1. 初始化与销毁
- 2. 等待操作(P)
- 3. 释放操作(V)
- 三、基于环形队列的生产者消费者模型
- 1. 环形队列原理
- 四、信号量的简单封装
- 五、基于 POSIX 信号量的环形队列实现
- 六、阻塞队列与环形队列
一、POSIX 信号量简介
POSIX 信号量和 System V 信号量的作用相同,都是用于多线程或多进程间的同步,保证多个线程访问共享资源时不会发生冲突。不同的是,POSIX 信号量除了可以支持进程间同步,还可以直接用于线程间同步,因此在多线程编程中更加常见。
当然这个信号量的操作本身就是原子的。
常见的 API 如下:
#include <semaphore.h>// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);// 销毁信号量
int sem_destroy(sem_t *sem);// 等待信号量(P 操作)
int sem_wait(sem_t *sem);// 释放信号量(V 操作)
int sem_post(sem_t *sem);
参数说明:
pshared = 0
表示线程间共享,非零表示进程间共享value
表示信号量初始值
二、POSIX 信号量的操作
1. 初始化与销毁
使用 sem_init()
初始化信号量时,需要指定初始值和是否进程间共享;使用 sem_destroy()
在使用完成后释放资源。
2. 等待操作(P)
调用 sem_wait()
会尝试将信号量的值减一,如果值为 0,则调用线程会被阻塞直到信号量大于 0。
3. 释放操作(V)
调用 sem_post()
会将信号量的值加一,并唤醒可能正在等待的线程,表示某个资源已经被释放。
三、基于环形队列的生产者消费者模型
在上一节我们介绍了基于 queue
的实现,它可以动态扩展容量。但在实际应用中,常常使用固定大小的缓冲区(如数组)实现一个环形队列。
1. 环形队列原理
环形队列通过数组实现,利用模运算来模拟“首尾相接”的特性。
- 环形队列在初始状态和结束状态可能相同,因此需要借助计数器或标记位来区分队列满和队列空。
- 也可以预留一个空位来表示“满”的状态。
- 在有了信号量后,计数的功能可以交由信号量完成,代码实现会更加简洁。
四、信号量的简单封装
在使用原始 sem_t
时,常需要重复调用初始化、等待和释放操作。为方便使用,我们可以对信号量做一个轻量级封装:
#include <iostream>
#include <semaphore.h>
#include <pthread.h>namespace SemModule
{const int defaultvalue = 1;class Sem{public:Sem(unsigned int sem_value = defaultvalue){sem_init(&_sem, 0, sem_value);}~Sem(){sem_destroy(&_sem);}void P() // 放出资源 p 相当于资源库--{sem_wait(&_sem); // 原子的}void V() // 回收资源 v 相当于资源库++{sem_post(&_sem); // 原子的}private:sem_t _sem;};}
这个类提供了 P()
和 V()
方法,分别对应 sem_wait
和 sem_post
,大大简化了信号量的调用。
五、基于 POSIX 信号量的环形队列实现
下面是一个环形队列 RingQueue
的实现,支持多生产者、多消费者。它利用两个信号量进行同步:
_room_sem
:表示剩余空间,生产者关心_data_sem
:表示已有数据,消费者关心
环形队列天然可以实现生产者与消费者之间的同步与互斥关系,因为信号量本身就是原子的,但是无法满足生产/消费自身之间的互斥关系,所以需要同时使用两把互斥锁来保证多个生产者、多个消费者之间的互斥。
#pragma once#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"using namespace SemModule;
using namespace MutexModule;static const int gcap = 5;template <typename T>
class RingQueue
{
public:RingQueue(int cap = gcap): _rq(cap), _cap(cap), _blank_sem(cap), _data_sem(0), _p_step(0), _c_step(0){}void Equeue(const T &in){// 生产者// 1. 申请信号量,空位置信号量_blank_sem.P();{LockGuard lockguard(_pmutex);// 2. 生产_rq[_p_step] = in;// 3. 更新下标++_p_step;// 4. 维持环形特性_p_step %= _cap;}_data_sem.V();}void Pop(T *out){// 消费者// 1. 申请信号量,数据信号量_data_sem.P();{LockGuard lockguard(_cmutex);// 2. 消费*out = _rq[_c_step];// 3. 更新下标++_c_step;// 4. 维持环形特性_c_step %= _cap;}_blank_sem.V();}private:std::vector<T> _rq;int _cap;// 生产者Sem _blank_sem; // 生产者在乎空位置int _p_step;// 消费者Sem _data_sem; // 消费者在乎有数据位置int _c_step;// 多生产多消费之间的互斥关系维护(锁)Mutex _cmutex;Mutex _pmutex;
};
六、阻塞队列与环形队列
在 生产者–消费者模型 的实现中,资源既可以看作一个 整体,也可以拆分为 小块。
整体角度:当我们把缓冲区视为一个完整的资源池,需要用 阻塞队列 来管理整体状态(满/空),解决线程之间的等待与唤醒问题。
局部角度:当我们把缓冲区中的数据看作一个个小块时,需要用 环形队列 来组织这些小块的存放与取出,保证空间高效利用,避免资源浪费。