当前位置: 首页 > ai >正文

信号量基础入门:并发控制的核心概念

问题的复杂性产生的根本原因在于,如 2.2 节所述,共享变量的访问始终是“单向信息流”。也就是说,一个进程可以分配新值或检查当前值,但这种检查不会为其他进程留下任何痕迹。结果是,当一个进程想要对共享变量的当前值作出反应时,在检查和随后执行反应之间,该值可能已被其他进程更改。换句话说,现有的通信机制对于手头的问题来说是不充分的,我们需要寻找更合适的替代方案。

这种替代方案通过以下方式引入:
a) 在共享变量中引入特殊用途的整数,我们称之为“信号量(semaphores)”。
b) 在构成各个进程的动作集合中添加两种新的基本操作,我们分别称之为“P 操作”和“V 操作”。这些操作始终作用于信号量,并代表并发进程访问信号量的唯一方式。

信号量本质上是非负整数。如果仅用于解决互斥(Mutual Exclusion)问题,则其值的范围甚至可以限制为“0”和“1”。荷兰物理学家兼计算机设计师 C.S. Scholten 博士证明了信号量在取更大值时具有广泛的应用价值。需要区分时,我们会将它们分别称为“二进制信号量(binary semaphores)”和“通用信号量(general semaphores)”。我接下来给出的 P 操作和 V 操作的定义不受这种区别的影响。

——Dijkstra 的笔记 EWD 123

引言

事实上,信号量是一个难以理解的概念。它是同步问题的核心,与互斥锁一起是最先学习的概念之一,但初次接触时往往难以理解。

通常,信号量可以用以下方式总结:

“信号量是一种通过 P 和 V 这两个特殊的原子操作来操作表示资源可用性的计数器,从而解决因共享资源并发访问而导致同步无法保证的问题的技术。”

但是,共享资源到底是什么?原子操作又是什么?资源的可用性、P 和 V 又是什么?
为了回答这些问题,我写下了这篇文章。

本文的目标是帮助理解并发编程的基础以及信号量的概念。

什么是共享资源?

当程序并行执行时,多个线程或进程同时访问的数据被称为“共享资源(shared resource)”。

  • 示例:内存中的变量、文件句柄、网络端口、队列等。

共享资源具有状态(state)。如果同时对这个状态进行访问和修改,可能会引发意外错误。

用打印机比喻来理解共享资源

假设有一台打印机。想象一下,两个人同时尝试使用这台打印机时会发生什么:

  • 如果 A 正在打印,而 B 在中途开始打印会怎样?
  • 可能会导致打印中断、输出重叠,或者打印出空白页。

这表明对共享资源的并发访问会引起冲突。

为什么需要原子操作?

那么,如何解决这个问题呢?
核心在于,即使多个线程同时访问,也要确保状态的一致性,即保证“原子性(Atomicity)”。

例如,将变量 x 加 1 的操作 x = x + 1 实际上可以分解为以下三个步骤:

  1. 读取 x 的当前值。
  2. 将其加 1。
  3. 将结果写回 x

即使是这种看似简单的操作,如果有其他进程在中间介入,结果可能会被破坏。例如,两个线程同时执行 x = x + 1 时,最终结果可能只增加 1 而不是预期的 2,甚至可能出现更少的增量。

这种互相竞争修改值的情况导致了意想不到的状态,这种情况被称为竞态条件(Race Condition)

即使是“看起来像一行代码的操作”,实际上也可能分为多个步骤执行,因此需要一种防止中间介入的手段。

这就是原子操作 的作用。原子操作是不可分割的单位操作,在执行过程中不允许任何外部干预。

再次用打印机比喻来理解

  • 当 A 向打印机发送打印请求时,只有在打印完全结束、失败或中止后,B 的请求才能开始处理。
  • 如果在 A 打印过程中 B 插入并开始打印,可能会导致输出混合或部分丢失的错误。
  • 因此,只有在“A 的打印状态”明确结束后,才能处理下一个请求(B),从而保证输出状态的一致性。

这就是原子性的要求条件。
在打印过程中,打印机的状态(State)应处于“使用中”的锁定(lock)状态,
只有在完全结束后才能进行下一个任务。

为了同时管理程序中的基于状态的资源 ,需要一个无法被中途打断的原子控制机制

好了,那么原子性是如何保证的呢?这就是我们今天的主题。

Dijkstra 的提议:什么是信号量?

特殊用途的整数(special-purpose integers)

正如之前打印机的例子所示,我们需要能够从外部明确控制和监控“正在使用中”的状态。然而,仅靠普通变量无法安全地管理这种状态。

原因如下:

  • 任何人都可以读取和写入变量。
  • 某个进程读取的值可能在下一刻被其他进程更改。
  • 换句话说,在读取值并对其做出反应的时间间隔内,状态可能会发生变化,而这种变化不会被反映出来。

因此,现有的方式是一种容易中断的“检查-决定-执行”流程。
为了克服这种结构性限制,Edsger W. Dijkstra 提出了一个新的解决方案。

引入一种特殊用途的整数作为共享变量,我们称之为“信号量(semaphores)”。
——Dijkstra, EWD 123

什么是信号量?

信号量(semaphore)不仅仅是一个简单的整数。
它是一个外部访问受到控制的、具有特殊用途的状态值。

其核心在于:“禁止直接访问,只能通过两种操作(P/V)间接控制。”

在构成各个进程的动作集合中,添加两种新的基本操作,我们分别称之为“P 操作”和“V 操作”。这些操作始终作用于信号量,并代表并发进程访问信号量的唯一方式。
——Dijkstra, EWD 123

这个特殊的整数只能通过以下两个操作进行操作:

  • P 操作 (Proberen,意为“测试”)
  • V 操作 (Verhogen,意为“增加”)

这两个操作遵循以下规则:

操作

含义

效果

P(s)

wait / acquire

如果信号量值为正,则减 1 并通过;如果为 0,则等待。

V(s)

signal / release

将信号量值增加 1。

这些操作始终以原子性 方式执行,即它们不会被任何中断或干扰打断。

这正是我们一直在寻找的“防止中途介入的机制”。
这就是信号量。

P/V 操作的定义

一个简单的实现如下:

// 信号量用整数值 s 表示
P(s): // waitwhile (s <= 0) wait;s = s - 1;V(s): // signals = s + 1; // 在此之后,如果有等待中的进程,需要唤醒它们

现代操作系统中,这一过程通过 futex、spinlock、sleep queue 等方式实现。

需要注意的是,在 P/V 操作伪代码中,s = s - 1s = s + 1while (s <= 0) wait; 条件检查部分必须作为不可分割的原子操作 执行。

如果 P(s)while (s <= 0) wait; 部分是通过持续占用 CPU 并反复检查条件是否满足的方式(即忙等待(busy-waiting 或 spin-waiting) ),那么这将非常低效地使用 CPU 资源。这是在浪费本可以用于其他有用任务的 CPU 时间。

因此,现代操作系统采用以下机制来更高效地处理这种“等待”过程,这些方法可以看作是现代异步处理的核心技术:
睡眠队列(Sleep Queue / Wait Queue)、上下文切换(Context Switching)、自旋锁(Spinlock)、以及 Futex(快速用户空间互斥锁)
这些方法的整体理论基础正是上述简单的操作。

总结一下,在现代操作系统中,信号量操作可以描述如下:

  • 1. 基本的等待/唤醒: 使用睡眠队列 高效地挂起和唤醒进程(避免忙等待)。
  • 2. 内部原子性保障: 在 P/V 操作短暂的执行期间,为了安全地修改信号量变量(如 s)或睡眠队列,内核内部可能会使用自旋锁 等机制。
  • 3. 性能优化(尤其是 Linux): 利用Futex ,当没有资源竞争时在用户空间快速处理,只有在发生竞争时才使用内核的睡眠队列功能,从而减少系统调用开销。

二进制信号量 vs 通用信号量

区分

含义

使用场景

二进制信号量

值:0 或 1

等同于互斥锁(Mutex)

通用信号量

值:0 或更大

有限资源(例如:数据库连接池)

  • C.S. Scholten 的通用信号量概念扩展。

  • 与互斥锁的区别: 拥有权限的概念 vs 状态驱动模型

让我们回到打印机的例子,使用信号量控制打印机。

在打印机示例中应用二进制信号量的过程如下:

  1. 用一个初始值为 s = 1 的信号量表示打印机的状态。
  2. 用户 A 调用 P(s),将 s 减为 0 并开始打印。
  3. 在用户 A 打印过程中,用户 B 调用 P(s),但由于 s <= 0,用户 B 进入等待状态。
  4. 用户 A 完成打印后调用 V(s),使 s = 1,用户 B 随即可以开始打印。

通过这种方式,信号量将资源的“状态”抽象为数字,并通过原子操作改变该数值,从而实现对并发的控制。

前面提到的二进制信号量 适用于办公室只有一台打印机的情况(互斥访问)。
s=1 表示“打印机可用”,s=0 表示“打印机正在使用”。

但是,如果办公室有多个相同性能的打印机(例如:3 台)该怎么办呢?
在这种情况下,虽然允许多个人同时使用打印机,但必须确保使用的打印机数量不会超过可用的数量。
这正是**通用信号量(General Semaphore)计数信号量(Counting Semaphore)**发挥作用的时候。

通用信号量 s 是一个非负整数值,用于表示可用资源的数量。

使用通用信号量控制 3 台打印机的示例
  1. 初始状态:

    • 办公室有 3 台打印机,因此信号量 s 的初始值设置为 3s = 3)。
    • 这表示“当前有 3 台可用的打印机”。
  2. 用户 A 请求使用打印机(执行 P(s) 操作):

    • 调用 P(s)
    • 当前 s 的值(3)大于 0,因此将 s 减 1(s = 2)。
    • 用户 A 分配到一台打印机并开始打印。(现在可用的打印机数量为 2)
  3. 用户 B 请求使用打印机(执行 P(s) 操作):

    • 调用 P(s)
    • 当前 s 的值(2)大于 0,因此将 s 减 1(s = 1)。
    • 用户 B 也分配到一台打印机并开始打印。(现在可用的打印机数量为 1)
  4. 用户 C 请求使用打印机(执行 P(s) 操作):

    • 调用 P(s)
    • 当前 s 的值(1)大于 0,因此将 s 减 1(s = 0)。
    • 用户 C 分配到一台打印机并开始打印。(现在可用的打印机数量为 0)
  5. 用户 D 请求使用打印机(执行 P(s) 操作):

    • 调用 P(s)
    • 当前 s 的值(0)小于或等于 0,因此用户 D 会在 P(s) 操作中的 while (s <= 0) wait; 条件下进入等待状态 ,直到有打印机可用。
  6. 用户 A 打印完成并归还打印机(执行 V(s) 操作):

    • 用户 A 完成打印后调用 V(s)
    • s 增加 1(s = 1)。(现在有 1 台打印机可用)
    • V(s) 操作完成后,之前在 P(s) 操作中等待的用户 D 被唤醒,并重新检查 s 的值。由于 s 现在为 1,用户 D 将 s 设置为 0 并开始使用打印机。

通用信号量的核心作用:

如上所述,通用信号量不仅仅是一个简单的二进制“锁定/解锁”机制,它还可以精确管理有限资源池的并发访问 ,并准确跟踪可用资源的数量

结语

信号量仍然是内核级同步的核心工具,同时也是基于状态的访问模型的典型代表。

信号量看似只是一个简单的整数,
但它却是系统中用于准确表示资源状态
安全控制资源 、以及以可预测的方式共享资源的唯一抽象手段

我们必须牢记,在这个小小的整数背后,
承载着无数进程的秩序、冲突避免和系统稳定性

http://www.xdnf.cn/news/7334.html

相关文章:

  • BGP选路
  • 常用ECSQL整理
  • ‌AT6558R-5N22北斗B1I单频导航芯片
  • 《深入理解数组名:sizeof(arr)、arr 和 arr 的区别》
  • 三种嵌入式开发常用的组网方式
  • 【C++】C++的IO流
  • 青岛地铁二号线列车运行图优化系统
  • AIGC与文本生成:人工智能写作的新纪元
  • Adminer:一个基于Web的轻量级数据库管理工具
  • Python | 需求预测模型
  • 使用 docker-volume-backup 备份 Docker 卷
  • plc基础知识整理(三菱)
  • PHP 实现连续子数组的最大和、整数中1出现的次数
  • 详解Oracle HASH CHAIN和HASH BUCKET
  • TS04:高性能四通道自动灵敏度校准电容触摸传感器
  • 【氮化镓】关态下负栅压对 p-GaN HEMTs 单粒子效应的影响
  • 智慧招生:实时数字人在院校招生中的应用
  • 上路兵线的理解-鳄鱼篇
  • 【工具推荐】--Git详解
  • LightRAG 由入门到精通
  • CSS- 4.5 css + div 布局 简易网易云音乐 官网布置实例
  • R 语言科研绘图第 49 期 --- 热力图-相关性
  • MySQL进阶篇-InnoDB引擎(超细)
  • 大模型预训练、微调、部署、推理用到的工具总结
  • Lambda 表达式底层实现机制 vs 成员函数/静态成员函数可替代性对比
  • 易境通散货拼柜系统:提高货代企业货物配载效率
  • python打卡day30@浙大疏锦行
  • 【强化学习】#6 n步自举法
  • Blaster - Multiplayer P65-PXX : 射击武器
  • 吉林省建筑工程专业技术人员职称评审实施办法