Skip to content

信号量

约 1636 个字 8 张图片 预计阅读时间 5 分钟

信号量 vs 互斥锁

简单来说:互斥锁(Mutex)是信号量(Semaphore)的一个特殊子集(二元信号量),信号量是互斥锁的通用扩展(计数信号量)。两者均用于解决多进程 / 多线程间的资源竞争问题,但适用场景、核心特性差异显著。

核心关系是:互斥锁 = 二元信号量(Binary Semaphore)

信号量的核心是一个**计数器 + P/V 操作**:

  • P 操作(wait):计数器 - 1,若计数器 < 0 则阻塞;
  • V 操作(post):计数器 + 1,若计数器≤0 则唤醒一个阻塞的进程 / 线程。

而互斥锁(Mutex)的本质是**计数器只能取 0 或 1 的信号量**:

  • 锁空闲时,计数器 = 1(资源可用);
  • 加锁(P 操作):计数器→0,其他线程 / 进程再加锁会阻塞;
  • 解锁(V 操作):计数器→1,唤醒一个阻塞的线程 / 进程。

几乎所有操作系统中,互斥锁都是基于二元信号量实现的 —— 互斥锁的 “加锁 / 解锁” 等价于二元信号量的 “P/V 操作”,只是封装了更贴合 “独占资源” 的语义(如所有权、递归加锁等)。

两者对比:

特性 信号量(Semaphore) 互斥锁(Mutex)
计数器取值 非负整数(0,1,2,...N) 仅 0/1(二元)
核心用途 控制**多个**共享资源的访问(如 10 个连接池) 保证**单个**资源的独占访问(如 1 个打印机)
所有权 无所有权(任意线程 / 进程可释放) 有所有权(仅加锁的线程 / 进程可解锁)
递归加锁 支持(多次 P 操作,需对应 V 操作) 部分支持(递归互斥锁),普通 Mutex 禁止递归加锁(会死锁)
适用场景 进程 / 线程间同步、有限资源池控制 进程 / 线程间互斥、临界区保护
典型示例 限制同时访问数据库的连接数(如最多 5 个) 保护单个文件的读写操作(同一时间仅 1 个线程写)

关键差异拆解

  1. “同步” vs “互斥”

    • 信号量:主打**同步**(让多个进程 / 线程按顺序执行),也可实现互斥;例:生产者生产数据后(V 操作),消费者才能消费(P 操作),控制 “生产 - 消费” 的顺序。

    • 互斥锁:主打**互斥**(保证同一时间只有一个执行者),无法实现复杂同步;例:多个线程写同一个文件,加互斥锁保证同一时间仅 1 个线程写,避免数据错乱。

  2. 所有权差异(最易踩坑)

    • 信号量:A 线程 P 操作获取信号量,B 线程可 V 操作释放 —— 无所有权限制,易引发逻辑错误;

    • 互斥锁:A 线程加锁,必须由 A 线程解锁(B 线程解锁会报错)—— 所有权保证更安全。

  3. 资源数量控制

    • 信号量:可设置计数器 = N,允许 N 个线程 / 进程同时访问资源(如 N=5,最多 5 个线程用连接池);

    • 互斥锁:仅允许 1 个线程 / 进程访问(计数器 = 1),本质是 “N=1 的信号量”。


多进程 / 多线程场景下的核心锁类型(按用途 + 特性分类)

在多进程 / 多线程同步场景中,锁的设计围绕 “资源独占、并发优化、场景适配” 三大目标,核心可分为 基础独占锁、并发优化锁、跨进程锁、特殊场景锁 四大类,下面按 “类型定义 + 核心特性 + 适用场景” 拆解,附选型优先级,新手也能快速匹配场景。


一、基础独占锁(核心基石,必掌握)

这类锁的核心是 “保证资源独占访问”,是所有同步锁的基础,适用于绝大多数基础场景。

锁类型 核心定义 核心特性 适用场景
互斥锁(Mutex) 二元锁(0/1),同一时间仅 1 个线程 / 进程持有,保证资源独占 ✅ 有所有权(仅加锁者可解锁)

✅ 获取失败则阻塞(让出 CPU)

❌ 不支持并行读
通用独占场景:文件读写、全局变量修改、临界区代码执行(读写均衡 / 写多读少)
递归互斥锁(RLock) 互斥锁的扩展,允许同一线程多次加锁(需对应次数解锁) ✅ 解决 “递归调用加锁” 问题

✅ 其他特性同 Mutex
递归函数、嵌套加锁场景(如函数 A 调用函数 B,两者都需加锁)
自旋锁(Spin Lock) 忙等版互斥锁,获取失败时不阻塞,循环检测锁状态(不放弃 CPU) ✅ 无上下文切换开销(性能极高)

❌ 自旋耗 CPU

✅ 多核场景优势明显
多核 CPU + 临界区极短(<10 微秒):内核态代码、高频内存操作、低延迟交易系统

关键提醒

  • 自旋锁 仅适用于多核 + 短临界区,单核 / 长临界区使用会导致 CPU 100% 占用;
  • 递归互斥锁需严格保证 “加锁次数 = 解锁次数”,否则会死锁。

二、并发优化锁(读多写少 / 多资源场景)

这类锁是基础锁的 “场景化优化”,解决基础锁 “全量串行” 导致的性能浪费问题。

锁类型 核心定义 核心特性 适用场景
读写锁(Read-Write Lock) 分读锁(共享锁)、写锁(排他锁):多读并行、写写 / 读写互斥 ✅ 读多写少场景性能远超 Mutex

✅ 可配置优先级(读优先 / 写优先)

❌ 实现复杂
读占比≥80% 的场景:配置中心、缓存查询、日志查看、商品库存查询
信号量(Semaphore) 计数锁(0~N),控制同时访问资源的线程 / 进程数(本质是 “多资源版 Mutex”) ✅ 无所有权(任意线程可释放)

✅ 支持同步 + 互斥

❌ 易误释放导致异常
有限资源池控制:连接池(最多 10 个连接)、线程池、限流(最多 5 个并发请求)
条件变量(Condition) 基于 Mutex 的扩展,实现 “等待 - 唤醒” 机制(非纯锁,配合锁使用) ✅ 精准唤醒指定线程 / 所有线程

✅ 解决 “轮询等待” 问题
生产者 - 消费者模型、任务排队执行、线程间同步通信

关键提醒

  • 信号量不是 “纯锁”,但常被归为同步锁范畴,不要用信号量实现简单互斥(无所有权,易出错);
  • 读写锁避免 “读锁升级为写锁”(先加读锁再加写锁会导致死锁)。

三、跨进程锁(多进程场景专属)

多线程锁(如 Python threading.Lock)仅在进程内有效,跨进程需用内核级 / 文件级锁。

锁类型 核心定义 核心特性 适用场景
文件锁(fcntl/portalocker) 基于文件的跨进程锁,通过内核维护文件锁状态 ✅ 跨进程 / 跨机器(网络文件)

✅ 兼容所有系统

❌ 性能略低(涉及文件 IO)
多进程共享文件、跨进程资源独占(如多进程写同一个日志文件)
系统 V 共享内存锁(shmctl) 基于 System V 共享内存的跨进程锁 ✅ 纯内存操作(性能高)

❌ 接口老旧、移植性差
老系统跨进程同步、高性能跨进程内存共享
POSIX 互斥锁(PThread Mutex) 跨进程的 POSIX 标准锁(需映射到共享内存) ✅ 性能高、接口统一

✅ 支持进程 / 线程级锁定
现代 Linux/Unix 多进程同步、跨进程临界区保护
分布式锁(Redis/ZooKeeper) 跨机器 / 跨集群的分布式锁(非系统级锁,属于应用层锁) ✅ 跨网络、跨集群

✅ 支持超时、重入

❌ 依赖中间件
分布式系统:分布式任务调度、跨机器资源独占、秒杀系统库存保护

关键提醒

  • Python 多进程推荐用 multiprocessing.Lock(封装了 POSIX 锁),无需手动实现文件锁;
  • 分布式锁优先选 Redis(高性能)或 ZooKeeper(高可靠),避免手动实现(易出超时 / 死锁问题)。

四、特殊场景锁(解决特定问题)

这类锁针对 “死锁、公平性、优先级” 等特殊需求设计,属于进阶用法。

锁类型 核心定义 核心特性 适用场景
公平锁(Fair Lock) 按 “先到先得” 顺序分配锁,避免线程饥饿 ✅ 解决 “锁抢占不均” 问题

❌ 性能略低(需维护等待队列)
对公平性要求高的场景:任务调度、资源分配
重入锁(Reentrant Lock) 同 “递归互斥锁(RLock)”,允许同一线程多次加锁 ✅ 避免递归调用死锁

✅ 有所有权保护
嵌套函数加锁、回调函数加锁
屏障锁(Barrier) 等待所有线程到达指定点后,再一起执行 ✅ 实现 “同步点” 机制

✅ 支持超时、重置
多线程协同任务:数据分片计算(所有分片计算完成后汇总)
死锁检测锁(Deadlock Detection Lock) 内置死锁检测逻辑,触发死锁时报警 / 解锁 ✅ 调试 / 监控场景实用

❌ 性能开销大
开发 / 测试阶段、复杂锁嵌套场景(定位死锁问题)

五、锁选型优先级(新手直接套用)

1. 多线程场景

  1. 通用独占 → Mutex(threading.Lock)
  2. 读多写少 → 读写锁(第三方库如rwlock);
  3. 递归 / 嵌套加锁 → RLock(threading.RLock)
  4. 多核 + 短临界区 → 自旋锁(Python 需模拟,C/C++ 原生支持);
  5. 线程同步通信 → Condition(threading.Condition)

2. 多进程场景

  1. 简单跨进程独占 → multiprocessing.Lock
  2. 多进程共享文件 → 文件锁(portalocker)
  3. 高性能跨进程内存共享 → POSIX 互斥锁 / System V 锁
  4. 跨机器 / 集群 → Redis/ZooKeeper 分布式锁

3. 避坑总原则

  1. 优先用 “最简单的锁”:能用水 Mutex 解决的,不用读写锁 / 信号量(减少复杂度);
  2. 跨进程锁优先选 “内核级”(如multiprocessing.Lock),而非手动实现文件锁;
  3. 自旋锁仅用于 “多核 + 极短临界区”,否则不如 Mutex 高效;
  4. 分布式锁必须加 “超时机制”,避免网络异常导致死锁。

总结

  1. 核心分类:基础独占锁(Mutex / 自旋锁)→ 并发优化锁(读写锁 / 信号量)→ 跨进程锁(文件锁 / 分布式锁)→ 特殊场景锁(公平锁 / 屏障锁);
  2. 选型核心:先看 “线程 / 进程”→ 再看 “读写比例 / 资源数量”→ 最后看 “特殊需求(公平性 / 递归)”;
  3. 关键提醒:锁的本质是 “牺牲并发换安全”,避免过度加锁(如无竞争的代码段加锁)。

并发机制

image.png

image.png

image.png

image.png

互斥量

image.png

TODO:底层原理是什么?如何阻塞?如何通知的?

条件变量

互斥量解决的是多线程并发读写的安全问题,条件变量解决的是多线程下协作的通知和等待问题。两者是配合关系,解决的不同问题。条件变量典型应用在生产消费问题上。

image.png

image.png

自旋锁

image.png