
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [7 ~> 环形队列](#7 ~> 环形队列)
-
- [7.1 技术背景与定义](#7.1 技术背景与定义)
- [7.2 2)核心原理解析](#7.2 2)核心原理解析)
- [7.3 实验](#7.3 实验)
-
- [7.3.1 POSIX 信号量的面向对象封装](#7.3.1 POSIX 信号量的面向对象封装)
- [7.3.2 多生产多消费(MPMC)环形队列实现](#7.3.2 多生产多消费(MPMC)环形队列实现)
- [7.4 关键特性提取](#7.4 关键特性提取)
- [7.5 总结](#7.5 总结)
- [7.6 返璞归真:如何看待信号量?](#7.6 返璞归真:如何看待信号量?)
-
- [7.6.1 站在一个更高的视角,重新看待信号量、互斥锁](#7.6.1 站在一个更高的视角,重新看待信号量、互斥锁)
- [7.6.2 基于环形队列的并发边界与时序约束](#7.6.2 基于环形队列的并发边界与时序约束)
-
- [7.6.2.1 空间解耦与并发态(常态)](#7.6.2.1 空间解耦与并发态(常态))
- [7.6.2.2 指针重合与同步态(极值状态)](#7.6.2.2 指针重合与同步态(极值状态))
- [7.6.2.3 单向推进的安全约束(生命周期原则)](#7.6.2.3 单向推进的安全约束(生命周期原则))
- 结尾
7 ~> 环形队列
一会儿实现的时候我们的文件名就叫"RingQueue"啦。
7.1 技术背景与定义
在多线程并发环境中,协调不同执行流对共享资源的访问是系统级开发的核心难点。根据文档描述,为解决这一问题,系统引入了互斥与同步机制。互斥机制(Mutex)保证任何时刻有且只有一个执行流能够进入临界区访问临界资源,以维护操作的原子性 。同步机制则在保证数据安全的前提下,使线程按照特定的顺序访问资源,有效避免线程饥饿并解决竞态条件 。
在生产者消费者模型中,引入阻塞队列(BlockingQueue)或环形队列(RingQueue)作为数据缓冲区,能够实现生产者与消费者的极度解耦,支持高并发并解决处理速度不均的问题 。相比于基于互斥锁和条件变量将整个队列作为单一临界资源的阻塞队列,环形队列结合 POSIX 信号量,能够实现更细粒度的资源管理与真正的并发访问 。
7.2 2)核心原理解析
信号量(Semaphore)的本质是一个描述临界资源数量的计数器 。POSIX 信号量通过原子的 P 操作(等待信号量,计数器减 1)和 V 操作(发布信号量,计数器加 1)来控制资源的预定与释放 。
基于数组模拟的环形队列生产消费过程,必须严格遵循四项核心运行原则,以维护逻辑闭环:
(1)为空时:环形队列没有数据,此时消费者和生产者指向同一位置,必须由放苹果的人(生产者)先运行 。
(2)为满时:环形队列没有空间,两者同样指向同一位置,必须由拿苹果的人(消费者)先运行,以腾出空间 。
(3)生产者不能套圈:生产者不能超越消费者,否则会覆盖尚未被消费的历史有效数据 。
(4)消费者不能超越生产者:消费者不能跑到生产者前面,否则会读取到已经废弃的脏数据 。
当队列既不为空也不为满时,生产者与消费者访问的是环形队列中的不同物理位置,此时两者可以实现真正的并发执行 。
环形队列的访问路径可视化如下:

7.3 实验
7.3.1 POSIX 信号量的面向对象封装
为了工程化调用,首先利用 RAII 思想对系统级原生的 sem_t 接口进行类化封装。
cpp
#pragma once
#include <iostream>
#include <semaphore.h>
class Sem
{
public:
Sem(int n)
{
sem_init(&_sem, 0, n); // 初始化信号量
}
void P()
{
sem_wait(&_sem); // 等待信号量,申请资源
}
void V()
{
sem_post(&_sem); // 发布信号量,释放资源
}
~Sem()
{
sem_destroy(&_sem); // 销毁信号量
}
private:
sem_t _sem;
};
7.3.2 多生产多消费(MPMC)环形队列实现
在多生产者多消费者模型中,存在三种关系:生产者与消费者的互斥与同步、生产者与生产者的互斥、消费者与消费者的互斥 。为了维护同类型角色间的互斥关系,不仅需要信号量,还必须引入互斥锁(Mutex)保护下标资源 _p_step 和 _c_step 。
cpp
template<typename T>
class RingQueue
{
private:
void Lock(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); }
void Unlock(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); }
public:
RingQueue(int cap)
: _ring_queue(cap), _cap(cap),
_space_sem(cap), _data_sem(0), // 生产者初始拥有全部空间,消费者数据为0
_p_step(0), _c_step(0)
{
pthread_mutex_init(&_p_mutex, nullptr);
pthread_mutex_init(&_c_mutex, nullptr);
}
void Enqueue(const T &in)
{
_space_sem.P(); // 1. 预定空间资源
Lock(_p_mutex); // 2. 竞争生产者下标锁
_ring_queue[_p_step++] = in;
_p_step %= _cap; // 模运算维持环状特性
Unlock(_p_mutex); // 3. 释放锁
_data_sem.V(); // 4. 增加数据资源
}
void Pop(T *out)
{
_data_sem.P(); // 1. 预定数据资源
Lock(_c_mutex); // 2. 竞争消费者下标锁
*out = _ring_queue[_c_step++];
_c_step %= _cap;
Unlock(_c_mutex); // 3. 释放锁
_space_sem.V(); // 4. 腾出空间资源
}
~RingQueue()
{
pthread_mutex_destroy(&_p_mutex);
pthread_mutex_destroy(&_c_mutex);
}
private:
std::vector<T> _ring_queue;
int _cap;
int _p_step;
int _c_step;
Sem _space_sem;
Sem _data_sem;
pthread_mutex_t _p_mutex;
pthread_mutex_t _c_mutex;
};
7.4 关键特性提取
(1)锁与信号量的申请顺序权衡 :在上述 Enqueue(入队列) 与 Pop 的实现中,必须"先申请信号量,再申请互斥锁" 。信号量的 P 操作本质是对资源的预定(买票) 。如果先加锁再申请信号量,会导致多个线程排队进入临界区后才能进行资源判断,退化为串行;而先申请信号量,则允许大量线程在临界区外并发地完成资源配额的分配,只有在最终修改同一时刻的数组下标时才产生锁竞争,极大提升了多线程环境下的并发效率 。
(2)隐式条件判断的精简 :基于信号量的环形队列内部无需进行 if 或 while 的容量条件判断。因为信号量作为原子计数器,只要 P 操作成功返回,就表明当前绝对有资源可供操作,条件判断逻辑被信号量机制前置且隐式地完成了 。
(3)解耦特性强化:使用环形队列,只要缓冲区不空且不满,多生产者和多消费者即可在不同下标处独立执行内存级别的存取操作,这突破了传统整体队列必须将队列视作同一临界资源的性能瓶颈 。
7.5 总结
Linux 系统级同步与互斥设计的终极目标是在安全与性能之间寻求最优解。传统的阻塞队列采用"互斥锁 + 条件变量"的模式,将状态检查与数据读写强行绑定在同一个大粒度的锁中,引发了不必要的线程等待。
环形队列引入 POSIX 信号量,通过 _space_sem 和 _data_sem 的双计数器机制,将"能否访问资源"的验证剥离出临界区,将其转化为线程并行的预定行为。在系统开发中,这种架构高度契合内核网络协议栈的环形缓冲区(如网卡接收环)以及高并发服务器任务队列的设计逻辑。它证明了在解决竞态条件时,通过精确分割数据结构(物理下标分离)并利用原子计数器前置资源分配,是突破单锁并发瓶颈的关键范式。
7.6 返璞归真:如何看待信号量?
7.6.1 站在一个更高的视角,重新看待信号量、互斥锁
- 1、刚刚写环形队列的生产者消费者模型的时候,为什么代码在临界区内部没有判断?
- 2、不管是生产者还是消费者的代码,我都没有判断条件!为什么?
因为我是先申请信号量的,信号量本身就是描述临界资源数量的------反过来说,只要我申请信号量成功了,就一定有我的资源,至于这个资源是谁,由下标或者什么来决定(申请信号量就是资源的预定机制)------阻塞队列那里,先申请锁;环形队列这里先申请信号量,也就是说,判断已经隐形地由信号量提前做了判断,所以在临界区不用做判断了。
- 3、访问环形队列的时候,可不可以强制地加一些判断呢?
在内部加一些和环形队列无关的条件判断:

资源如果整体使用,就有一种信号量:二元信号量(相当于互斥锁,信号量计数器为1)。
- 4、怎么看待抢票、生产者消费者模型?
阻塞队列是STL里面的容器,没有头尾,只能整体使用(因此只能使用互斥锁进行保护),用二元信号量预定资源,访问完了再V操作。
(阻塞队列的话)有资源给我用,允许给我访问,但是当前资源能不能给我用,(作为一个队列,还有其它像队列是否未满等判断)还需要二次判断。
得先申请资源,预定了,允许我用------用不等于修改,修改还需要判断我当前有没有修改的权力。
资源可以被我使用 != 资源可以被我修改(需要做判断)
阻塞队列:能不能新增、删减数据,都需要判断条件。
让线程等待也必须在临界区内部。
7.6.2 基于环形队列的并发边界与时序约束
7.6.2.1 空间解耦与并发态(常态)
当队列处于非空且非满状态时,读指针(消费者下标)与写指针(生产者下标)指向不同的物理内存区块。此时读写操作在空间地址上完全解耦,两个执行流互不干扰,系统处于真正的并发运行状态。
7.6.2.2 指针重合与同步态(极值状态)
当且仅当系统运行至容量的极值边界时,读写指针发生重合。此时并发必须退化为严格的串行同步,触发以下两种互斥逻辑:
- (1)队列全空(读指针追平写指针):此时无有效数据可读。必须强制消费者阻塞等待,由生产者优先获取控制权执行写入。
- (2)队列全满(写指针追平读指针):此时无空闲槽位可写。必须强制生产者阻塞等待,由消费者优先获取控制权执行读取,以释放内存缓冲区。
7.6.2.3 单向推进的安全约束(生命周期原则)
为维持环形队列的逻辑闭环,指针的推进必须服从两项绝对约束:
- (1)防内存覆盖约束:生产者的指针推进绝对不能超越消费者。若发生越界,将导致尚未被处理的有效数据在物理层面被直接覆盖。
- (2)防脏读约束:消费者的指针推进绝对不能超越生产者。若发生越界,将导致执行流读取到内存中已废弃的脏块数据。
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux线程】Linux系统多线程(六):<线程同步与互斥>线程同步(上)
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
