Linux 线程同步与互斥(四) POSIX信号量,基于环形队列的生产者消费者模型

目录

一、POSIX信号量

原理介绍

[互斥锁 ≈ 二元信号量](#互斥锁 ≈ 二元信号量)

信号量和P/V操作原理

[信号量的底层数据结构 :](#信号量的底层数据结构 :)

[P 操作的底层逻辑 :](#P 操作的底层逻辑 :)

[V 操作的底层逻辑:](#V 操作的底层逻辑:)

二、介绍信号量接口

sem_init

sem_destroy

[sem_wait(P 操作)](#sem_wait(P 操作))

[sem_post(V 操作)](#sem_post(V 操作))

[使用注意事项 :](#使用注意事项 :)

三、实现环形队列的单生产者单消费者模型

环形队列原理讲解

方案一:

方案二:

环形队列的本质

联系信号量

四、代码实现:

封装信号量

单单模型

RingQueue.hpp​编辑

Main.cc

多多模型:

RingQueue.hpp

​编辑

Main.cc

派发任务

Task.hpp​编辑

Main.cc

RingQueue.hpp


一、POSIX信号量

原理介绍

这里我们要讲的 POSIX 信号量是 System V 信号量的原理本质完全一样的,只是接口、用法、生命周期不一样。不管是 POSIX 信号量 还是 System V 信号量,它们底层的核心逻辑完全相同,都是一个计数器

信号量的本质是一个用于描述可用资源数量的计数器 ,它可同时实现线程 / 进程间的同步与互斥,核心逻辑是将资源是否就绪的判断前置到临界区之外,通过 P、V 两个原子操作完成资源的申请与释放:

  1. 申请资源执行 P 操作 ,将计数器减 1,若计数器大于 0 则申请成功,线程可进入临界区访问共享资源;若计数器为 0 则资源耗尽,线程阻塞等待。
  2. 释放资源执行 V 操作 ,将计数器加 1,若有阻塞线程则唤醒其中一个继续执行。

访问临界资源的操作必须严格放在 P 操作之后、V 操作之前,形成 "P - 临界区 - V" 的结构,由于 P 操作是原子性的资源预占机制,只要申请成功,就必然能获得一份临界资源的访问权,从根本上避免了临界区竞争带来的竞态问题,既保证了互斥访问,又实现了线程间的有序同步。

POSIX 信号量是 System V 信号量在原理、语义、行为完全一致。区别只在 "用法和系统管理" 上

  1. System V 信号量
  • 接口复杂古老:semget / semop / semctl
  • 生命周期随内核持续,进程关了还在,必须手动删,并且较笨重、容易遗留
  1. POSIX 信号量
  • 接口简单:sem_init / sem_wait / sem_post / sem_destroy
  • 分为无名信号量(基于内存,线程 / 亲缘进程用)和有名信号量(文件标识,非亲缘进程用)
  • 生命周期随进程/手动销毁,用起来像变量,清爽

互斥锁 ≈ 二元信号量

互斥锁(Mutex)≈ 二元信号量(Binary Semaphore)

二者在功能上完全等价------ 都能将资源计数设为 1,实现同一时刻仅一个线程进入临界区,保证互斥访问 ;但在设计语义、所有权规则、底层实现和适用场景上存在本质差异。

从核心机制看:

  1. 二元信号量本质是值为 1 的计数工具,通过 P(减 1 申请)、V(加 1 释放)操作实现资源管理,无所有权约束,任意线程可执行 P/V,既可用作互斥,也能协调线程顺序同步;
  2. 互斥锁是专为互斥设计的原语,核心是所有权机制,严格遵循 "谁加锁、谁解锁",仅持有锁的线程可解锁,从根源避免误解锁、死锁风险。

从底层实现看:

  1. 互斥锁依赖 CPU 原子指令(如 Swap/exchange)或内核态原语(如 Linux futex),轻量且底层;
  2. 二元信号量是上层通用同步原语,**底层通常封装互斥锁,包含计数器、保护计数器的互斥锁与等待队列,**通过 P/V 操作完成原子的资源申请与释放,资源不足时阻塞,释放时唤醒等待线程。

从适用场景看:

  1. 二元信号量更灵活,可兼顾互斥与同步(如生产者消费者模型的顺序协调),但无所有权约束,使用不当易出逻辑错误;
  2. 互斥锁更专一,仅用于临界区互斥,所有权机制使其更安全、易排查问题,是多线程编程中保护共享资源的首选。

简言之,二者在单纯实现互斥时效果一致,但互斥锁是为互斥量身打造的安全原语,更底层、更专一;二元信号量是通用同步原语的特例,更灵活、更上层,不可完全等同。

信号量和P/V操作原理

我们可以用一段简单的伪代码,把信号量和 PV 操作的底层逻辑原理讲述一下 :

信号量的底层数据结构 :

这是信号量的核心三要素:

  1. count:资源计数器,也就是信号量的本质,用来描述可用资源的数量,是 PV 操作的核心操作对象。

  2. lock:一把锁,专门用来保护 count 的修改,避免多个线程同时改 count 引发竞态条件,保证 PV 操作的原子性。

  3. thread_queue:阻塞等待队列,当资源耗尽(count=0)时,申请资源的线程会被挂起,放到这个等待队列里,等待被唤醒。

问题一 : 信号量的数据结构里面那个阻塞队列是在 Linux 内核态里吗?

是的,信号量的数据结构里的 "阻塞队列" 是在内核态里维护的。因为把线程 "挂起(阻塞)" 是操作系统内核的权限。用户态没有权限做这些事:把自己放入等待队列、让 CPU 切走、

让自己睡眠、这些都必须进入内核态才行。所以信号量的等待队列,必然是由内核维护的。

P 操作的真实流程是 : 用户态调用 sem_wait → 进入内核态使用系统调用 → 内核拿到信号量结构 → 检查 count → 大于0直接拿走资源,返回用户态/等于 0把当前线程加入 wait_queue

(阻塞等待队列中) → 内核把线程阻塞 → 直到执行 V 操作(sem_post)→ 内核唤醒。

P 操作的底层逻辑 :

P 操作是申请资源:

  1. 加锁是前提:必须先给 count 加锁,否则多个线程同时执行 P 操作,会出现 count 的读写不一致,导致竞态错误,这是保证 P 操作原子性的核心。
  2. 资源充足时直接占用:如果 count>0,说明有空闲资源,直接把 count 减 1(代表占用了一份资源),然后解锁返回,线程可以正常进入临界区访问共享资源。
  3. 资源不足时阻塞等待:如果 count=0,说明资源已经被占满,当前线程无法申请到资源,会被系统挂起,放入信号量的阻塞等待队列,主动放弃 CPU,不再占用时间片。
  4. 唤醒后重试:当其他线程执行 V 操作唤醒该线程后,线程会从挂起点恢复,回到lock(),重新走一遍 P 操作的流程,直到成功申请到资源为止。

伪代码里 P 操作的goto lock(),本质是阻塞线程被唤醒后的重试机制:通常会唤醒队首的一个线程,线程被唤醒后,不会直接拿到资源,而是要重新竞争锁、重新判断 count,避免了 "唤醒后资源又被抢占" 的问题,保证了资源分配的公平性和正确性。

V 操作的底层逻辑:

V 操作是释放资源:

  1. 同样先加锁:释放资源时修改 count,同样需要加锁保证原子性,避免和其他线程的 P/V 操作冲突。
  2. 计数器 + 1:把 count 加 1,代表归还了一份资源,让可用资源数增加。
  3. 解锁并唤醒等待线程:解锁后,如果阻塞等待队列里有阻塞的线程,会唤醒队首的一个线程,让它重新参与资源竞争(也就是回到 P 操作的goto lock()处)。
  4. V 操作永远不会阻塞:V 操作只是释放资源,不管 count 原来的值是多少,都会执行 + 1,不会让线程挂起,这是 P 和 V 操作的核心区别。

下面我们就来学习一下POSIX信号量的接口,使用POSIX信号量的时候都需要包信号量的头文件#include <semaphore.h>

二、介绍信号量接口

sem_init

sem_init 是 Linux 下 POSIX 信号量体系中,初始化信号量的核心接口,专门用于线程间或亲缘进程间的同步互斥。成功时返回 0,失败返回 -1,并设置错误码。

  1. 第一个参数 sem_t *sem 是信号量对象指针,传入要初始化的信号量变量地址,后续所有信号量操作都基于这个指针。

  2. 第二个参数 int pshared 代表共享属性,如果传参0那么表示线程间共享,如果传参非零,那么表示进程间共享,本文要使用的场景是线程间共享,所以第二个参数传参0。

  3. 第三个参数是信号量的初始值,信号量是一把计数器,你想让这把计数器从多少开始,那么就在第三个参数传参多少,但是在互斥场景下通常设为 1 (表示为二元信号量,等价互斥锁),在生产者消费者模型下生产者信号量可以设为 value =缓冲区大小,控制生产速度,避免队列满。消费者信号量可以设为 value=0,控制消费速度,避免队列空。

sem_destroy

那么当使用完成信号量之后,确保后续不再使用信号量了,那么使用sem_destroy接口,传入信号量的地址即可释放销毁信号量

sem_wait(P 操作)

sem_wait 是 POSIX 信号量体系中执行 P 操作(申请资源)的核心接口,对应信号量的减计数器、申请资源逻辑,是实现线程 / 进程同步互斥的关键函数。成功返回 0,代表成功申请到资源,计数器已减 1,失败:返回 -1,并设置错误码。

对第一个参数信号量 sem 执行P 操作(申请资源):

  • 若信号量计数器 count > 0**:将 count 减 1,函数立即返回,线程可进入临界区**
  • 若信号量计数器 count = 0**:线程会阻塞等待(挂起,放入信号量的等待队列),直到其他线程执行 sem_post(V 操作)释放资源、唤醒该线程,才会继续执行**

sem_post(V 操作)

sem_post 是 POSIX 信号量体系中执行 V 操作(释放资源) 的核心接口,对应信号量的加计数器、释放资源、唤醒等待线程逻辑,是 sem_wait 的配对函数,二者共同完成信号量的同步互斥。成功返回 0,代表成功释放资源,计数器已加 1,失败返回 -1,并设置错误码。

对信号量 sem 执行V 操作(释放资源):

  • 将信号量计数器 count 原子性加 1
  • 若有线程因 sem_wait 阻塞在该信号量的等待队列中,唤醒队首的一个线程,使其重新竞争资源
  • 永远不会阻塞:无论计数器当前值是多少,函数都会立即返回

使用注意事项 :

  1. 必须成对使用:sem_wait(P 操作)必须和 sem_post(V 操作)一一对应,否则会导致信号量计数器永久为 0,引发死锁
  2. 信号量合法性:传入的 sem 必须是已通过 sem_init 正确初始化的信号量,否则行为未定义
  3. 中断处理:sem_wait 可能被信号中断,需手动处理重试逻辑
  4. 二元信号量对应互斥锁:当初始化信号量 value=1 时,sem_wait 等价于互斥锁的 lock(),sem_post 等价于 unlock()
    5. sem_wait () 会阻塞,sem_post () 不会阻塞。
  • sem_wait(P 操作)去申请资源,如果资源不够(计数器 = 0),线程直接卡住不动,挂起等待,直到有人释放资源,才会醒过来。
  • sem_post(V 操作)释放资源,不管现在资源是多少,直接 +1,然后立刻返回,哪怕没人在等待,它也直接做完就走,它永远不会卡住线程。

三、实现环形队列的单生产者单消费者模型

环形队列原理讲解

  1. 既然是基于环形队列的生产者消费者模型,那么我们就要首先了解什么是环形队列,我们又该用什么手段实现这个环形队列呢?有两种方案如下
  1. 环形队列是一个环形的队列,那么我们可以使用一个指针 tail 作为环形队列的尾部,tail 的作用是指向环形队列尾部的格子,一个 head 作为环形队列的头部,那么 head 的作用是指向要放数据的格子,最开始的时候我们让 tail 和 head 指向一个格子表示环形队列为空,即对应上图中的最左边红色即代表格子中已经放了数据,白色代表格子中没有数据,那么我们先放一个数据,此时head向后移一格,tail 不动,即上图中的中间的环形队列对应的情况

  2. 接下来我们向环形队列的格子中持续放数据,那么 head 就持续的向后移动,直到 head 和 tail重合,此时对应上图中的右边的环形队列对应的情况,所以此时环形队列为满喽,那么问题也就来了,什么问题呢?

  3. 最初环形队列为空的时候 head 和 tail 重合,现在环形队列为满,那么 head 和 tail 也重合了,所以我们就无法区分环形队列究竟是满了还是没有满,如果满了继续向格子中放数据,那么就会覆盖原有格子中已经存在的数据,所以上面设计方法是不可行的,因为无法判断环形队列究竟是空还是满,那么我们该如何做呢?

方案一:

采用一个计数器记录当前有多少个格子已经放了数据,和总的格子数目进行比对,如果相等,那么则说明环形队列满了,否则则说明环形队列没有满

方案二:

空一个格子,原理如下

首先我们判断环形队列的代价是浪费一个空间,所以也就注定了会有一个格子始终是不存放数据的,那么开始为空的时候,我们让 head 和 tail 指向同一个位置,即对应上图的最左边

接下来我们开始放一个数据,那么 head 位置放数据之后,head 向后移动一个格子,那么对应上图中间的情况

那么为满的情况是head的下一个位置是tail,所以接下来我们开始持续放数据,每次放数据前都要判断head的下一个位置是是否是tail,如果不是那么说明此时环形队列还没有满,所以就向格子中放数据,于是格子就持续被放数据直到head的下一个位置是tail,那么此时代表环形队列在浪费一个格子的情况下已经被放满了,即对应上图的最右侧的情况,那么如何出数据呢?如下

那么出数据的时候要时刻判断 tail 是否等于 head,如果不等于,那么说明此时环形队列不为空,可以出格子中的数据,所以此时从最左侧的情况开始从格子中出数据,判断 tail 是否等于 head ,不等于,所以出格子中的数据,此时将 tail 位置对应格子的数据弹出,然后tail向后走一个格子,此时对应上图的中间的情况

那么继续出格子中的数据,判断 tail 是否等于 head,不等于,那么就可以持续将 tail 位置对应格子的数据弹出,然后 tail 向后走一个格子,直到 tail 等于 head,那么说明此时环形队列为空,所以停止出格子中的数据

所以 tail 等于 head 代表环形队列为空,head 的下一个为 tail 代表环形队列在浪费一个格子的情况下为满

所以我们应该如何模拟这个环形队列呢?数组模拟,逻辑如下,但是今天由于POSIX信号量的存在,所以我们并不需要使用如下的判断逻辑以及空出一个空间的代价,具体原因后面解释

环形队列的本质

  1. 环形队列的本质是一个固定大小的数组,通过模运算来模拟 "首尾相连" 的环形结构,从而实现高效的 FIFO(先进先出)队列操作。

2. 环形队列的物理存储就是一段连续的数组内存,图中就是一个长度为 12 的数组,用来存放队列元素。数组的大小决定了队列的最大容量(这里是 12 个元素),所有入队、出队操作,本质都是对这个数组的读写

  1. pos1 是队头指针,指向下一个要出队的元素位置。pos2 是队尾指针,指向下一个要入队的空位置。两个指针初始都为 0,随着入队 / 出队操作不断向后移动

  2. 数组是线性的,下标范围是 [0, 11](长度 12),要实现 "到末尾后回到开头" 的环形效果,就必须用模运算:

  • 入队时:pos2 = (pos2 + 1) % 12
  • 出队时:pos1 = (pos1 + 1) % 12

当指针走到数组末尾(下标 11),+1 后模% 12,结果为 0,自动回到数组开头,完美模拟环形

联系信号量

那再和信号量联系起来:

我们一共需要用到两种信号量 : 空槽信号量blank_sem 和 数据信号量data_sem

信号量与队列状态的映射 :

  • 队空时:blank_sem = N(全是空槽),data_sem = 0(无数据)
  • 队满时:blank_sem = 0(无空槽),data_sem = N(全是数据)
  • 中间状态:blank_sem + data_sem = N,二者之和恒等于队列容量,同步队列的元素数量

我们再来看一下对应环形队列的 4 条核心规则,这是信号量设计的设计和逻辑基础:

  1. 队空状态:head == tail,生产者(tail)和消费者(head)指向同一位置,此时无数据可消费,只有空槽可生产
  2. 队满状态:(tail+1)%N == head,二者再次指向同一位置,此时无空槽可生产,只有数据可消费
  3. 生产者约束:不能套圈超过消费者(即不能覆盖未消费的数据)
  4. 消费者约束:不能超过生产者(即不能消费未生产的数据)
  5. 中间状态:head != tail 且未队满,生产、消费可并发执行

生产者线程(放数据)

生产者先执行 P(blank_sem) 申请空槽,拿到一个空的存储位置后,向空槽里写入数据(相等于生产数据);写入完成后,执行 V(data_sem) 释放生产的数据资源,通知消费者有新数据可用了。

消费者线程(取数据)

消费者先执行 P(data_sem) 申请数据,拿到数据消费后,再执行 V(blank_sem) 释放空槽,相当于把刚用掉的空槽归还,让生产者继续往这个位置填数据。

简单说就是:生产者用空槽换数据,消费者用数据换回空槽,两个信号量的 PV 操作刚好形成了一个完美的资源循环闭环。

四、代码实现:

我们先封装一下信号量,然后在此基础上写基于环形队列的生产消费模型

封装信号量

单单模型

单生产者 - 单消费者(SPSC)模型

RingQueue.hpp

Main.cc

运行结果1:

运行结果2:

多多模型:

多生产者 - 多消费者模型

这个基于信号量的环形队列的多多模型和我们之前用的普通阻塞队列(非环形、用 queue / 锁 + 条件变量) 的改法不一样,普通阻塞队列,只改 main 函数开多线程就能直接多生产者多消费者,内部逻辑基本不动。

但我们今天学的 环形队列 + 信号量 的多多模型就不行、必须额外加锁。因为两套模型里,"临界区保护" 的位置不一样。

先回忆上一篇的用互斥锁和条件变量实现的阻塞队列的核心代码:

我们可以看相互整个 push/pop 的逻辑全在锁里面,生产者之间天然互斥,消费者之间天然互斥,甚至生产消费之间也互斥,所以即使外面有多个线程,内部不用改,天然就是多生产者多消费者安全。因为它从根上就用锁把所有操作包围了。

我们再来看现在这套基于信号量的环形队列的多多模型:

关键点在于多生产者同时执行 _productor_step++ 时,会出现自增操作的竞态,导致下标错乱、数据覆盖。多消费者同时执行 _consumer_step++ 时,会出现重复消费、漏消费。信号量只能控制资源数量,不能保护临界区的原子操作。_productor_step++ 和 _consumer_step %= _cap 是非原子操作,多线程并发执行时,CPU 时间片切换会导致中间状态被其他线程读取,引发逻辑错误。

多个生产者可以同时通过 P() 申请到空位资源,同时冲进 Enqueue() 里面。比如说生产者 A 和生产者 B,都成功 _blank_sem.P(),都申请到了资源,两个线程同时执行_productor_step ++,本来应该 A 写下标 0,B 写下标 1,结果两人读到同一个step,同时写入同一个下标,这就会导致数据直接覆盖丢失。而单单模型因为只有单个生产者和消费者所以这种情况不会发生。

所以信号量 P() 只管资源的预约,多个线程可以同时通过 P 预约资源,互斥锁管的是能不能同时写入读取数据,同一时间限制只有 1 个人操作下标 + 写数组。

那加锁解锁在P / V之前还是之后好?

锁必须加在 P 操作之后,V 操作之前。

如果放在 P 操作之前,V 操作之后,如下图:

如果生产者在拿到锁之后再执行 P (),当 P 发现申请资源不足时会根据信号量底层原理把自己也就是生产者线程挂起进入阻塞队列,此时生产者线程持有的锁不会自动释放;Linux 下的多线程并发里,生产者线程如果正处于阻塞队列中被阻塞并持有锁时,会导致整个调度器的线程调度被卡住。CPU 不会调度到后续线程。所以表现出来的效果就是消费者线程被系统暂停调度,导致它根本没机会运行锁。

因为是多线程,所以在打印时我们需要给每个线程起个名字方便我们查看,

还有两个问题需要解决,第一个问题是多个线程同时向显示屏打印时会造成打印错乱

因为线程是操作系统调度的最小单位,CPU 会在多个线程之间快速切换(时间片轮转)。当多个线程同时执行 std::cout << ... 时,CPU 可能在一个线程打印到一半时,切换到另一个线程,导致两个线程的输出穿插、重叠、乱序。

因此我们给打印语句加锁,使同一时间只允许一个线程持有锁打印数据。我们给打印操作加锁后:线程 A 拿到锁 → 完整打印一行 → 释放锁,线程 B 才能拿到锁 → 完整打印一行 → 释放锁,这样就保证了每一行输出都是完整、有序的,不会被其他线程打断。


第二个问题就是原来的 data 是每个生产者线程自己的局部变量,3 个生产者,就有 3 个互不干扰的 data,同时也会出现 product1=1、product2=1、product3=1 这样重复乱序的数据,虽然局部变量 data 在线程栈里,每个线程自己独一份,互不干扰,但是 3 个生产者都会从1开始数:1、2、3 最终队列里全是重复数字,虽然不会崩溃,但不符合多生产者有序编号需求。

所以我们现在将 data 设置为全局变量并给 data 加锁了,这样生产者线程在生产数据时就不会出现重复数字。

至此多多模型的代码就修改完成了

RingQueue.hpp

需要注意的是RingQueue.hpp包含的这两个头文件是我们自己封装的

Main.cc

打印结果:

派发任务

下面我们再把任务结合起来,让线程生产任务和完成任务:

Task.hpp

Main.cc

那在Main.cc中就作如下修改:

RingQueue.hpp

在RingQueue.hpp中作如下修改:

运行结果:

运行结果符合多生产者 - 多消费者模型的预期

相关问题

问题1 : 所以基于环形队列的生产者消费者模型是以信号量为核心的吗?和锁就没有关系了是吧?在这个模型中就不用锁了是吗?

基于环形队列的生产者消费者模型以信号量为核心 实现同步与互斥,表面代码中无需显式使用互斥锁,但底层并非完全脱离锁机制,而是将锁封装在了信号量内部;由于环形队列的头尾指针属于共享资源,多线程并发修改时必须保证操作原子性,而信号量作为高级同步原语,其内部已包含资源计数器、用于保护计数器与等待队列的内核态锁、管理阻塞线程的内核态阻塞队列,以及原子化的 P/V 操作逻辑,在执行sem_wait申请资源时会原子递减计数器,资源不足则线程进入阻塞队列等待,执行sem_post释放资源时会原子递增计数器并唤醒等待线程,这一系列底层操作已通过内置锁和原子指令保障了共享队列访问的线程安全,因此上层代码无需额外添加互斥锁,额外加锁反而会造成性能损耗、增加死锁风险,这也是标准生产者消费者模型普遍采用信号量实现,而非手动编写互斥锁的核心原因。

问题2 : 我们上一篇的普通的生产者消费者模型用的是锁,没有用信号量?和现在有什么区别?

在基于环形队列的生产者消费者模型中,代码层面只使用信号量而不出现显式的互斥锁,并非完全脱离锁机制,而是因为信号量作为高级同步原语,内部已封装了用于保护计数器的锁、原子化的P/V操作、内核阻塞队列以及线程唤醒逻辑,因此无需在业务代码中额外加锁;执行sem_wait时会原子递减资源计数,资源不足则线程进入内核阻塞队列等待,sem_post则原子递增计数并唤醒等待线程,这一整套底层逻辑已通过内置锁和原子操作保障了环形队列头尾指针等共享资源访问的线程安全,使得入队、出队操作无需手动加锁保护。

而传统的生产者消费者实现通常采用互斥锁加条件变量的方案,需要在代码中显式调用加锁、解锁、等待和唤醒接口,手动维护队列状态与线程同步。

两种实现方式底层都依赖锁及内核同步机制,区别仅在于信号量将锁与同步逻辑高度封装,上层代码简洁无感知,而锁加条件变量的方案则需要开发者显式管理锁和同步条件,二者本质都是通过内核态的阻塞、唤醒与原子操作实现线程安全与执行有序性。

问题3 : 那信号量和互斥锁加条件变量这两个模型哪个更优?

互斥锁加条件变量与信号量两种方案均能实现生产者消费者模型,底层表达能力完全等价, 都可完成线程等待唤醒、临界区保护与线程协作,二者本质上都是操作系统提供的同步原语;在工程实践中,信号量模型更具优势,其仅需通过sem_wait与sem_post即可实现逻辑,代码简洁清晰,无需编写while循环判断队列状态,且底层已封装锁、阻塞队列与原子操作,降低了误操作与死锁风险,P/V 操作的语义也更贴合线程同步的业务逻辑;而锁加条件变量的方式需要手动管理互斥锁、自行判断同步条件,代码更繁琐且易出错,理论上还会多出用户态与内核态的切换开销,性能略逊一筹,因此信号量模型是实现生产者消费者更标准、更推荐的选择。

五、总结

本文系统讲解了POSIX信号量的原理及其在环形队列生产者消费者模型中的应用。信号量本质是带计数器的同步原语,通过P/V原子操作实现资源申请与释放,其内部已封装锁机制,可替代显式互斥锁+条件变量的实现方式。针对环形队列的特性,采用空槽信号量(blank_sem)和数据信号量(data_sem)的双信号量方案:生产者通过P(blank_sem)获取空槽,生产后V(data_sem);消费者通过P(data_sem)获取数据,消费后V(blank_sem)。对于多生产者多消费者场景,需额外加锁保护队列指针操作。相比传统实现,信号量方案代码更简洁,底层通过内置锁和原子操作保证线程安全,是更优的同步方案。

谢谢大家的观看!

相关推荐
SPC的存折2 小时前
8、Docker镜像瘦身
运维·docker·容器
uElY ITER2 小时前
VS与SQL Sever(C语言操作数据库)
c语言·数据库·sql
Highcharts.js2 小时前
在 React 中使用 useState 和 @highcharts/react 构建动态图表
开发语言·前端·javascript·react.js·信息可视化·前端框架·highcharts
抠脚学代码2 小时前
Linux开发-->驱动开发-->字符设备驱动框架(2)
linux·运维·驱动开发
何中应2 小时前
Promehteus如何指定数据路径
运维·prometheus·监控
热爱Liunx的丘丘人2 小时前
Ansible的Playbook案例一
linux·运维·服务器·ansible
likerhood2 小时前
java中的return this、链式编程和Builder模式
java·开发语言
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【排序贪心】:拼数
c++·算法·贪心·csp·信奥赛·排序贪心·拼数
浪客川2 小时前
【百例RUST - 014】Trait
服务器·网络·rust