云计算百科
云计算领域专业知识百科平台

POSIX信号量与环形队列

一、先回顾:信号量到底是个什么东西

1.1 信号 ≠ 信号量

信号和信号量,是「老婆」和「老婆饼」的关系 —— 没有任何关系。

信号(signal)信号量(semaphore / 信号灯)
本质 一种进程间异步通知的方式 一个计数器
作用 通知你"出事了" 表明临界资源中资源数量还剩多少
比喻 门铃 停车场的余位显示屏

1.2 POSIX 与 System V

  • sem_init / sem_wait / sem_post 这一套属于 POSIX 标准(可移植操作系统接口)
  • 之前进程间通信学的 semget / semop / semctl 属于 System V 标准
  • POSIX 系列是最常用的,System V 基本已经算被淘汰了
  • 知道它俩是两套标准就可以,理论完全一样,没有区别

✔ 关键点:以前讲 System V 信号量时是在"进程间通信"里讲的,今天在"线程"里讲 POSIX 信号量 —— 理论一模一样,只是作用的范围从进程变成了线程(sem_init 的 pshared 参数控制)。


二、★ 本质结论一:计数器 = 对特定资源的「预订机制」

2.1 电影院:买票那一刻,座位就是你的了

用电影院举例:

你是屁股坐在座位上,这个座位资源才属于你?还是票买到了,座位就是你的了?
票买成功的那一刻,座位就是我的了。哪怕今天这场电影我不去看,那个座位也必须给我留着。

由此得出本章最重要的一条结论:

信号量的本质是一个计数器,是对特定资源的「预订机制」。

在这里插入图片描述

2.2 落到代码上就是三行

int sem = 50; // 放映厅 50 个座位 → 计数器初值 = 资源总数

// 买票 = 申请资源 = P 操作
sem—; // 申请一个
if (sem < 0) // 减到负数 = 没票了
阻塞挂起; // 排队等别人退票(不是忙等!)

// 退票 / 用完归还 = V 操作
sem++;

💡 这样就能保证:进放映厅的人数永远不会超过座位数,最多相等。再通过合理的调度让不同线程坐不同座位 —— 大家就能同时访问放映厅里不同的座位了。

2.3 放映厅最怕的两件事

事故技术含义
票卖多了 计数器减到负数还放人进来 → 超卖 → 数据被覆盖
票号重复 两个线程拿到同一个位置 → 同时读写同一块内存 → 数据不一致

✔ 这两条正好对应后面环形队列的两条铁律:
① 不要访问同一个位置(互斥);② 不要放入过多的进程进来(限流)。

2.4 为什么"买票"不算访问资源?

这是这套比喻最妙的地方:

  • 买票 = 预订成功,票还没用 → 只是把"还剩多少份"这个判定给做掉了
  • 真正进场落座 = 访问临界资源

💡 所以信号量把「资源够不够」的判定,提前到了访问临界资源之前,并且是原子完成的。这一点在第十节会再拔高一次。


三、二元信号量就是互斥锁

继续往下推:

  • 普通放映厅有 50 个座位 → sem = 50 → 多元信号量
  • 超级 VIP 放映厅只有 1 个座位 → sem = 1 → 二元信号量

sem = 1 时:你申请一次成功了,整个放映厅就是你的;申请失败就等着。

二元信号量的本质,不就是互斥吗?

而互斥我们已经学过了 —— 所以 mutex 可以看作二元信号量的一个特例实现。

✔ 记法:信号量只有 1 或 0 两种状态的,叫二元信号量,它就是互斥。


四、★ 本质结论二:两种使用场景,决定你用 mutex 还是 sem

这是本章第二个重要结论,也是整个选型的分水岭。

在这里插入图片描述

场景一:把目标资源整体使用

典型例子就是上一章我们写的 基于条件变量的阻塞队列。

“我们的阻塞队列里,std::queue 是容器适配器,它内部封装的类里面有头指针、有尾指针,还要做空间的二次扩容。你在 pop 的时候它可能正在 push,正在二次扩容 —— pop 不就失败了吗?所以那种队列它不能够并发起来,只能被某一个生产者或某一个消费者单独使用。”

所以阻塞队列必须整体加锁 → 用 mutex 或二元信号量。

场景二:把目标资源按块分批使用

“我们访问内存空间读数据写数据,饭也得一口一口吃,空间我给你申请一大堆,你也是一个一个过的。”

如果一块大资源可以被划分成一小块一小块,而且每个线程访问不同的块,那就完全没有冲突 —— 可以让多个线程同时进来。

要做的事情无非两点:① 不要放过多的进程/线程进来;② 不要让多个线程访问同一个位置。

→ 这时候用多元信号量(sem = N)。

选型结论

资源使用方式用什么理由
整体使用,一次只许一个人完整占用 mutex / 二元信号量 必须串行
按块分批使用,各线程访问不同块 多元信号量 可以真正并发

五、为什么 P / V 必须原子:信号量本身也是临界资源

这一步推导特别关键,是整个信号量能成立的前提。

5.1 推导链

  • 所有线程访问资源之前,都得先看信号量
  • 信号量又必须被所有人看到 → 它是个共享变量
  • 共享变量 ++ / — 不是原子的(线程互斥那一章已经用三条汇编证明过)
  • 所以 —— 信号量本身也是临界资源!
  • 老师原话:“我们信号量不能是一个简单的 int count 加加、减减,那不行,那并不是原子的。”

    5.2 于是有了 P 和 V

    操作语义本质
    P 操作 申请资源(sem_wait) 对信号量的值做 –,必须原子
    V 操作 释放资源(sem_post) 对信号量的值做 ++,必须原子

    💡 P / V 来自荷兰语(Proberen 尝试 / Verhogen 增加),Dijkstra 提出的。国内教材通用 P/V 记法,代码里叫 sem_wait / sem_post。

    5.3 能不能自己用「锁 + 计数器」实现一个信号量?

    可以。 一把锁保护一个 int count,就能模拟信号量的行为。

    但:不重要。关键是"想访问资源必须先过信号量这一关" ——
    P 成功 = 预订成功 = 里面一定有一份资源是给你留着的。


    六、铺垫:环形队列,以及"空和满"的老问题

    6.1 复习:环形队列长什么样

    • 它也是个队列,所以一定有头有尾(head / tail)
    • 初始时 head == tail
    • 往尾部插入元素,tail 往后移;移到末尾就绕回开头 —— 这就是"环"

    6.2 致命问题:空和满指向同一个位置

    空:head == tail
    满:head == tail ← 一模一样,分不出来

    经典的两种解决方案:

    方案做法代价
    方案 1 加一个计数器 int count:入队 count++,出队 count–空时 count == 0,满时 count == N 多一个变量要维护
    方案 2 空一个位置:tail + 1 == head 视为满,永远留一个格子不用 浪费一个槽位

    6.3 物理实现:固定长度数组 + 模运算

    课件板书给的是最朴素也最实用的做法:

    int head = 0;
    int tail = 0;

    tail++; tail %= N; // 入队后绕回
    head++; head %= N; // 出队后绕回

    但是:“你往后移不就超范围了吗?随时都可能崩掉。所以 tail++ 之后不管是什么,直接模上 N。”

    💡 逻辑上是环,物理上就是一个长度为 N 的数组 + 模运算。当然你也可以用单链表首尾相连做,没必要。

    6.4 但今天,我们不判断空满

    “我今天要讲的和环形队列有关,但我们不需要再去判断什么为空为满了,压根就不用关心 —— 因为我今天要完成的是基于环形队列的生产者消费者模型,你到底有没有满、有没有空,我们是有其他方案来衡量的。这个方案就叫做信号量。”


    七、★ 追逐游戏:四条约定

    把环形队列比喻成一张大圆桌,每个格子就是一个盘子:

    • 我是生产者,往盘子里放苹果
    • 你是消费者,从盘子里拿苹果
    • 我从尾部开始放,你从头部开始拿

    7.1 游戏是这样进行的

  • 盘子全空时,两个人指向同一个位置 → 你得让我先生产(你拿也拿不到,里面压根没有)
  • 我一直放、一直放,放满了 → 两个人又指向同一个位置
  • 这时候我不能再放了(再放就把历史上的苹果覆盖了,盘子放不下会掉)
  • 换你拿,你拿拿拿,拿空了 → 两个人又回到同一个位置
  • 又轮到我生产 —— 这是一场追逐游戏
  • 操场跑圈的比喻:你站着不动,我一直跑,当我套你整整一圈时,我们就在同一个位置了。

    在这里插入图片描述

    7.2 四条约定(缺一不可)

    约定内容为什么
    约定 1 为空,生产者先运行 空的时候消费者拿不到任何东西
    约定 2 为满,消费者先运行 满的时候生产者再放就会覆盖数据
    约定 3 生产者不能把消费者套一个圈以上 最多并驾齐驱;套一圈就等于覆盖了所有历史数据
    约定 4 消费者不能超过生产者 超过生产者 = 跑到前面去拿,拿到的全是无效数据(“警察跑到小偷前面去了,这就很滑稽”)

    约定 3、4 合起来说的其实是一件事:两个人的相对距离永远不能超过一圈。

    ✔ 记住一句话:约定 1、2 管"谁先跑",约定 3、4 管"谁不能超过谁"。


    八、两个结论:什么时候可以「一起跑」

    三个问题,答案直接给出结论。

    思考 1:只要不访问同一个位置,能不能同时进行?

    能。 我在第 6 个位置放苹果,你在第 1 个位置拿苹果 —— 完全不冲突,生产消费可以真正同时发生。

    思考 2:什么时候两个人会指向同一个位置?

    “这一个问题只要想明白了,环形队列的问题就理解一半了。”

    答案:只有两种情况 —— 为空,或者为满。
    因为 head 和 tail 只有在为空和为满时才会重合,其他任何时候都不会。

    思考 3:为空 / 为满的时候谁先跑?

    • 为空 → 消费者没法消费 → 只能让生产者先运行
    • 为满 → 生产者没法生产 → 只能让消费者先运行

    注意这不只是"先后"问题,还必须是互斥的:不能我生产者正在生产的时候你就靠进来了。

    于是得到两条结论

    结论 1:环形队列【不为空 && 不为满】 → 生产和消费可以【同时进行】!
    结论 2:环形队列【为空 || 为满】 → 生产和消费需要【同步 + 互斥】!

    💡 这两条结论的"历史意义"在于:它把环形队列的问题,从"怎么判断空满"转化成了"怎么保证这 4 个约定"。
    答:用信号量来保证。


    九、信号量的两张票:sem_blank 与 sem_data

    在动手写代码之前,先把"生产者关心什么、消费者关心什么"分清楚。

    9.1 生产者关心「空位」,消费者关心「数据」

    “我放苹果,你拿苹果。你当然最关注的是苹果,而我作为放苹果的人,最关注的是空盘子。”

    角色关心的资源信号量初值
    生产者 环形队列里空的位置 sem_blank N(刚开始全空)
    消费者 环形队列里有效数据 sem_data 0(刚开始没数据)

    在这里插入图片描述

    9.2 代码骨架

    // 生产者
    P(sem_blank); // 空位减一,P 不到就阻塞
    // 走到这里,一定有空间!
    ring[p_step] = data;
    p_step++; p_step %= N;
    V(sem_data); // 数据加一,通知消费者

    // 消费者
    P(sem_data); // 数据减一,P 不到就阻塞
    // 走到这里,一定有数据!
    data = ring[c_step];
    c_step++; c_step %= N;
    V(sem_blank); // 空位加一,通知生产者

    9.3 完美对称

    生产者:P(空位) → 生产 → V(数据)
    消费者:P(数据) → 消费 → V(空位)

    ✔ 双方各自 P 自己关心的资源、V 对方关心的资源 —— 这就是两个信号量全部的流转逻辑。
    ✔ 而且注意顺序:P 在加锁之前(多生产多消费时)。先预定、再排队 —— 判定资源是否就绪,根本不需要加锁。第十三节专门论证这一点。


    十、接口:POSIX 信号量的四件套

    #include <semaphore.h>

    sem_t sem; // ① 类型:sem_t

    int sem_init(sem_t *sem, int pshared, unsigned int value); // ② 初始化
    // pshared = 0 → 线程间共享(我们今天用的)
    // pshared ≠ 0 → 进程间共享
    // value → 信号量初始值(计数器初值)

    int sem_destroy(sem_t *sem); // ③ 销毁

    int sem_wait(sem_t *sem); // ④ P 操作:值减 1
    int sem_post(sem_t *sem); // ⑤ V 操作:值加 1

    10.1 三个要点

    • sem_t 就是一个类型,跟 pthread_mutex_t 一样,定义一个对象就是一个信号量
    • sem_wait = P,sem_post = V —— 名字不同,语义完全一样
    • 返回值:0 表示成功,非 0 表示失败

    10.2 为什么只需要重点学 POSIX 这一套

    “POSIX 信号量主要用来做线程间的同步。当然如果你想做到进程间同步也可以 —— 用共享内存,把那个地址强转成 sem_t* 类型,用 sem_init 去初始化就行。”

    所以 System V 那一套可以不看了,POSIX 这套既能做线程也能做进程(配合共享内存)。

    10.3 sem_wait 内部干了什么

    sem_wait(&sem);
    // 底层:sem 的值 —
    // ├─ 减完 >= 0 → 申请成功,继续往后走
    // └─ 减完 < 0 → 当前线程被【阻塞】,直到信号量值重新 > 0 才被唤醒

    💡 这就是"预订":sem_wait 返回了,说明资源一定有你的;sem_wait 阻塞了,说明资源还没到位,你连门都不用进。


    十一、把信号量封装成 Sem

    // Sem.hpp
    #pragma once
    #include <iostream>
    #include <semaphore.h>

    namespace SemModule
    {
    const int default_value = 1;

    class Sem
    {
    public:
    Sem(int value = default_value) { sem_init(&_sem, 0, value); } // pshared = 0
    void P() { sem_wait(&_sem); }
    void V() { sem_post(&_sem); }
    ~Sem() { sem_destroy(&_sem); }
    private:
    sem_t _sem;
    };
    }

    ✔ 封装完就三件事:构造给初值、P()、V()。
    ✔ 之后在环形队列里直接用 Sem 对象,不用再碰裸接口。


    十二、代码 v1:单生产单消费的环形队列

    12.1 先看上层怎么用(自上而下设计)

    SemModule::Sem blank_sem(cap); // 或者直接在 RingQueue 内部构造
    RingQueue<int> *rq = new RingQueue<int>(5);

    pthread_create(&p, nullptr, producer, rq); // 一个生产者
    pthread_create(&c, nullptr, consumer, rq); // 一个消费者

    生产者往环形队列里 Enqueue,消费者从里面 Pop —— 两种角色、一个交易场所,全齐了。

    12.2 RingQueue 的成员

    template <typename T>
    class RingQueue
    {
    private:
    std::vector<T> _ring_queue; // 底层容器:固定长度数组(不是 queue!)
    int _cap; // 容量上限 N

    Sem _blank_sem; // 生产者关心:空位置数量,初值 = N
    Sem _data_sem; // 消费者关心:数据数量, 初值 = 0

    int _p_step; // 生产者下标,初值 0
    int _c_step; // 消费者下标,初值 0
    };

    💡 为什么用 vector 而不是 queue? 因为要能按下标随机访问(_ring_queue[i] = in)。
    queue 只能 push / front / pop,拿不到下标,也做不了"两个线程访问不同位置"这件事 —— 这正是它只能整体使用的原因。

    12.3 两个接口(v1:无锁)

    void Enqueue(const T &in) // 生产者
    {
    _blank_sem.P(); // ① 申请空位(没有就阻塞)
    _ring_queue[_p_step++] = in; // ② 一定有位置,直接生产
    _p_step %= _cap; // ③ 维持环形特性
    _data_sem.V(); // ④ 数据 +1,通知消费者
    }

    void Pop(T *out) // 消费者
    {
    _data_sem.P(); // ① 申请数据(没有就阻塞)
    *out = _ring_queue[_c_step++]; // ② 一定有数据,直接取走
    _c_step %= _cap; // ③ 维持环形特性
    _blank_sem.V(); // ④ 空位 +1,通知生产者
    }

    12.4 单生产单消费为什么不需要加锁

    “他们两个从生产到消费的过程都是原子的,没有人干扰我 —— 尤其是单生产单消费。只有当它俩访问同一个位置时才会出现竞争,那一刻用信号量来保证互斥与同步;不访问同一位置时,我们互不干扰。”

    而什么时候会访问同一个位置?只有为空或为满的时候 —— 而那两个时刻,信号量已经把其中一个线程挡住了(sem 值为 0,P 不过去)。所以冲突根本不会发生。

    12.5 跑起来看现象

    实验一:生产者快、消费者慢(每 2 秒消费一个)

    生产 1 2 3 4 5 ← 一瞬间把队列打满(cap = 5)
    (生产者阻塞:blank_sem 已经 0)
    消费 1 ← 2 秒后
    生产 6 ← 空出一个位置,立刻补上
    消费 2
    生产 7
    消费 3
    …

    ✔ 现象:一瞬间拉满,之后"消费一个、生产一个",严格交替 —— 这就是同步被可视化了。

    实验二:生产者慢、消费者快

    消费者一上来就 P(data_sem) → 值为 0 → 直接阻塞
    生产 1 → 消费 1 → 生产 2 → 消费 2 …

    ✔ 现象:生产者"生产一个",消费者才"能消费一个" —— 消费者被空队列卡住了。

    💡 这两组实验的唯一目的:让你肉眼看到同步。真实高并发跑起来,两个线程就是各种交叉,看不出来了。


    十三、★ 先加锁还是先 P?——电影院买票的第二课

    单生产单消费不需要锁。但一旦扩到多生产多消费,就必须加锁。于是问题来了:

    // 写法 1:先加锁,再 P
    mutex.Lock();
    sem.P();
    ...
    sem.V();
    mutex.Unlock();

    // 写法 2:先 P,再加锁
    sem.P();
    mutex.Lock();
    ...
    mutex.Unlock();
    sem.V();

    13.1 先加锁错吗?—— 不错,但低效

    因为加锁就是在维护互斥关系:多个生产者先自己争出一个胜利者,再由这个胜利者去和消费者竞争 —— 逻辑上没毛病。

    13.2 但效率差在哪:看电影的两种排队方式

    方式 A(先加锁):所有人在电影院门口排队,一个个进去检票,轮到你才掏手机买票。买到了进去,买失败了大家一起等。

    方式 B(先 P):所有人先把票抢了,没抢到票的人连排队的资格都没有;抢到票的人再依次在门口排队入场。

    哪种效率高?明显是 B。

    13.3 为什么 B 更快

    先加锁(A)先 P(B)
    锁的作用范围 包含了"申请资源"这一步 只包含"访问资源"这一步
    排队等待的线程在干什么 干等,什么都没做 已经在原子地申请信号量,把资源准备好了
    关键差异 抢到锁的人慢慢申请资源,其他人干等 资源被一次性瓜分,大家手持门票排队

    ✔ 而且信号量不会超申请:sem 初值是几,最多就放几个线程进来。这个"限流"是原子完成的,不需要锁保护。

    老师结论:先用信号量把资源瓜分掉,再让拿到资源的人一个个进来。谁拿着信号量,谁就一定能访问;访问过程一定是连续的,因为加锁了,没人打扰你。

    13.4 结论

    正确顺序:先 P(预定资源),后 Lock(排队进入)。

    💡 换个说法就是本文第四节的那句话:把"资源够不够"的判定,提前到访问临界资源之前。


    十四、代码 v2:多生产多消费,改了哪里

    14.1 指导思想:缺什么补什么

    按 321 原则盘点一下 v1 已经维护了哪些关系:

    关系v1(单生产单消费)v2(多生产多消费)
    生产者 — 生产者 不存在(只有一个) ✖ 缺,要加锁
    消费者 — 消费者 不存在(只有一个) ✖ 缺,要加锁
    生产者 — 消费者 ✔ 信号量已保证 ✔ 不变

    “当你明白了生产者消费者模型,你现在就知道代码里缺什么 —— 缺生产者和生产者之间、消费者和消费者之间的互斥关系。”

    14.2 加两把锁

    Mutex _p_mutex; // 维护【生产者之间】的互斥
    Mutex _c_mutex; // 维护【消费者之间】的互斥

    ✔ 为什么是两把而不是一把?
    一把也能保证互斥,但会把生产者和消费者互相串起来(生产者持锁时消费者也进不来),并发度白白降低。分成两把,各管各的。

    14.3 最终版代码

    void Enqueue(const T &in) // 生产者
    {
    _blank_sem.P(); // ① 先瓜分资源(原子判定)
    _p_mutex.Lock(); // ② 再排队(生产者之间互斥)
    _ring_queue[_p_step++] = in; // ③ 生产(一定有位置)
    _p_step %= _cap;
    _p_mutex.Unlock();
    _data_sem.V(); // ④ 通知消费者
    }

    void Pop(T *out) // 消费者
    {
    _data_sem.P(); // ① 先瓜分资源
    _c_mutex.Lock(); // ② 再排队(消费者之间互斥)
    *out = _ring_queue[_c_step++]; // ③ 消费(一定有数据)
    _c_step %= _cap;
    _c_mutex.Unlock();
    _blank_sem.V(); // ④ 通知生产者
    }

    💡 V 放在解锁之前还是之后都可以。课件里放在解锁之后(先 Unlock 再 V),“生产完之后 V,然后再释放锁”(放锁内)—— 两种都对,因为 V 只是把计数器加一,不涉及临界区。


    十五、★ 反向理解:为什么信号量的代码里完全没有判断?

    这是整节课最想让你想通的一问。

    把两份代码摆在一起看:

    // ◀ 上一章:条件变量版(有判断)
    pthread_mutex_lock(&_mutex);
    while (IsFull()) { // ← 在【临界区内部】判断
    pthread_cond_wait(&_product_cond, &_mutex);
    }
    _block_queue.push(in);
    pthread_mutex_unlock(&_mutex);

    // ▶ 今天:信号量版(没有判断)
    _blank_sem.P(); // ← 判定被塞进了这一句里
    _ring_queue[_p_step++] = in;

    15.1 判定去哪了?

    “因为信号量本来就是用来描述临界资源数量的。信号量申请失败,就是条件不满足。”

    条件变量版信号量版
    判定语句 while (IsFull()) 没有
    判定在哪 锁内(临界区里) 锁外(P 操作内部)
    判定形式 读共享变量 + 比较 原子地对计数器减一
    阻塞方式 cond_wait(挂到条件变量队列) P 减到负数(挂到信号量等待队列)

    ✔ 两者本质完全一样,只是"判定资源是否就绪"这件事从锁内搬到了锁外。

    15.2 这就是"信号量的另一个本质"

    在这里插入图片描述

    信号量把对「临界资源是否存在」的判定,以原子性的形式,呈现在访问临界资源之前就完成了。

    这句话是本章的最高总结。它的三个含义:

  • 判定提前了 —— 不用先进临界区再看有没有资源,门口就拦住了
  • 判定原子了 —— 减一这个动作不可分割,不会出现两个线程都判定成功
  • 判定和访问分离了 —— 所以可以先 P 后加锁,锁只保护"访问"这一段
  • 15.3 那能不能用「锁 + 条件变量」实现环形队列?

    能,完全可以。 但没必要 —— 有更贴合场景的工具。

    15.4 为什么这个场景能用信号量,上一个场景不能?

    回答是关键:

    “因为今天的环形队列,是我们自己定义成固定大小的,我们自己用自己的下标去访问,把它的资源看成了一小块一小块。
    而上一章的阻塞队列,我们把它看成一个整体 —— 倒不是说我们想把它看成整体,而是它只能被看成一个整体,因为那个队列会自动扩容,里面还有各种类内成员。”

    阻塞队列(std::queue)环形队列(vector + 下标)
    能否按下标切分 ✖ 不能(只能 push/pop) ✔ 能(ring[i])
    会不会自动扩容 ✔ 会(扩容时 pop 会失败) ✖ 固定长度
    只能整体使用? ✔ 是 ✖ 不是
    适配的技术 mutex(或二元信号量) 多元信号量

    十六、退化:sem 初值为 1 时,它就是互斥锁

    最后一个思考题:

    如果环形队列的容量不是 N 而是 1,会发生什么?

    答案是退化:

    • 数组只有 1 个元素 → 生产者下标和消费者下标永远是同一个
    • sem_blank = 1、sem_data = 0
    • 照样 P / V / P / V 跑,但它已经不是并发了,而是串行的

    “这不就是互斥锁了吗?这不就是你的环形队列当中只有一块,退化成我们上一章讲的阻塞队列了吗?”

    16.1 由此得到的第二个收获

    ✔ 二元信号量也能实现"整体使用"的同步互斥 —— 而且天然带互相通知:

    生产者:P(blank=1) → 生产 → V(data=1)
    消费者:P(data=1) → 消费 → V(blank=1)

    “我生产完通知你,你消费完通知我 —— 用二元信号量就可以对一个大资源整体使用,而且还能互相通知。”

    这正是第四节那张选型表的闭环:整体使用 → mutex / 二元信号量;分块使用 → 多元信号量。


    十七、总结速查表

    问题答案
    信号和信号量有关系吗 ✖ 没有(老婆 vs 老婆饼)
    信号量的本质 一个计数器,是对特定资源的预订机制
    电影院比喻买的是什么 买票 = 预订;票到手座位就是你的,人不去也留着
    sem = 1 叫什么 二元信号量,本质就是互斥
    资源整体使用用什么 mutex 或二元信号量
    资源按块分批使用用什么 多元信号量
    为什么信号量本身也是临界资源 它要被所有线程看到、要被 ++/–,而 ++/– 不是原子的
    P / V 是什么 P = sem_wait(–,申请)/ V = sem_post(++,归还),都必须原子
    环形队列空和满的麻烦 head == tail 分不出来 → 加 count 或空一个位置
    但今天还需要判断空满吗 ✖ 不需要,信号量替代了判断
    环形队列的四条约定 ① 空生产者先跑 ② 满消费者先跑 ③ 生产者不能套消费者一圈以上 ④ 消费者不能超过生产者
    什么时候能同时跑 不为空 && 不为满
    什么时候要同步互斥 为空 || 为满
    生产者关心什么资源 空位置,sem_blank 初值 N
    消费者关心什么资源 数据,sem_data 初值 0
    单生产单消费要加锁吗 ✖ 不要,信号量已经够了
    多生产多消费要加几把锁 两把(生产者一把、消费者一把)
    先加锁还是先 P ★ 先 P,后加锁(先瓜分资源再排队)
    为什么信号量代码里没有判断 申请失败 = 条件不满足,判定被原子地塞进了 P 里
    信号量把什么提前了 把"资源是否存在"的判定,以原子形式呈现在访问之前
    sem_blank 初值改成 1 会怎样 退化成互斥锁(环形队列只剩一个格子)

    写代码时的一条主线

    ① 资源能按块拆吗?
    ├─ 能(自己控下标、固定大小)→ 用信号量
    │ 生产者:P(空位) → 生产 → V(数据)
    │ 消费者:P(数据) → 消费 → V(空位)
    └─ 不能(容器整体、会扩容) → 用 mutex + 条件变量

    ② 是单生产单消费吗?
    ├─ 是 → 信号量足够,不用锁
    └─ 否 → 再补两把锁,维护"生产者之间""消费者之间"的互斥

    ③ 加锁和 P 谁在前?
    → 永远【先 P 后加锁】:先预定,再排队

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » POSIX信号量与环形队列
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!