mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1295 字
3 分钟
信号量(Semaphore)
2026-06-13

一、为什么需要信号量#

想象一个场景:你的服务同时打开 50 个数据库连接,数据库服务器直接报错「too many connections」。你加了个连接池,但池子本身也需要限制——总不能让 1000 个请求同时抢 10 个连接。这就是信号量要解决的问题:控制同时做某件事的线程数量

没有信号量会怎样?最常见的情况是资源耗尽。数据库连接、文件句柄、外部 API 配额——这些都是有限的。如果不对并发访问加以限制,系统在高负载下会雪崩:线程争抢资源导致超时,超时触发重试,重试加剧争抢,恶性循环。

更隐蔽的问题是「惊群效应」。100 个线程同时等待一个连接释放,连接归还时 100 个线程全被唤醒,但只有 1 个能拿到,剩下 99 个又回去睡觉。信号量通过维护一个计数器和等待队列,精确地唤醒一个等待者,避免了这种无谓的竞争。

二、现实类比#

带剩余车位显示的停车场。入口大屏显示「剩余车位 3」,车进去一辆,数字减一;车出来一辆,数字加一。显示 0 时,后面的车必须在门口排队等。信号量就是那个数字牌——它不关心谁在停车,只关心还有几个空位。

三、核心思想#

信号量本质上是一个带两个原子操作的计数器:

  • acquire:计数器减一,如果已经为零则阻塞等待
  • release:计数器加一,如果有等待者则唤醒一个
sequenceDiagram participant T1 as 任务 1 participant S as 信号量 (max=2) participant T2 as 任务 2 participant T3 as 任务 3 T1->>S: acquire (计数: 2→1) T2->>S: acquire (计数: 1→0) T3->>S: acquire (阻塞 — 计数=0) T1->>S: release (计数: 0→1) S->>T3: 唤醒 (计数: 1→0)

关键数据结构只有一个计数器加一个等待队列,复杂度如下:

操作复杂度说明
acquireO(1)有许可时直接递减;计数为零时阻塞
releaseO(1)递增计数器,唤醒一个等待者
公平性取决于实现FIFO 或任意顺序
空间O(1) + O(等待者数)计数器 + 等待队列

四、变体与对比#

模式关系区别
互斥锁(Mutex)信号量 max=1 时行为类似互斥锁互斥锁有所有权语义——只有获取者才能释放;信号量是匿名计数器,任何线程都能 release
限流器(Rate Limiter)都限制访问限流器控制时间维度的吞吐量(每秒 N 次);信号量控制并发数量(同时 N 个)
对象池(Object Pool)池大小本质上是一个信号量对象池额外管理资源的创建、复用和销毁;信号量只管计数
背压(Backpressure)信号量可以实现背压背压是一种流控思想,信号量是具体实现手段
Note

最大值为 1 的信号量虽然行为类似互斥锁,但不应该替代互斥锁。互斥锁的所有权语义能防止其他线程意外释放,还支持优先级继承——信号量做不到这些。

五、多语言实现#

Go:用带缓冲的 channel 模拟信号量#

Go 没有原生的信号量类型,但带缓冲的 channel 天然具备信号量的语义——写入即获取,读出即释放。

func processWithLimit(items []string, maxConcurrent int) {
// 带缓冲 channel 充当信号量
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
sem <- struct{}{} // acquire:缓冲区满时阻塞
go func(s string) {
defer wg.Done()
defer func() { <-sem }() // release:读出释放一个位置
process(s)
}(item)
}
wg.Wait()
}

channel 方式的优势在于与 select 自然组合,可以同时处理超时和取消:

select {
case sem <- struct{}{}:
// 获取成功
case <-ctx.Done():
// 上下文已取消
}

TypeScript:基于 Promise 的异步信号量#

class Semaphore {
private queue: (() => void)[] = [];
private count: number;
constructor(private max: number) {
this.count = max;
}
async acquire(): Promise<void> {
if (this.count > 0) {
this.count--;
return;
}
// 计数器为零,排队等待
return new Promise<void>((resolve) => this.queue.push(resolve));
}
release(): void {
const next = this.queue.shift();
if (next) {
next(); // 唤醒等待者,计数器不变
} else {
this.count++; // 没有等待者,恢复计数
}
}
}
// 配合 try/finally 确保释放
async function withSemaphore<T>(sem: Semaphore, fn: () => Promise<T>): Promise<T> {
await sem.acquire();
try { return await fn(); }
finally { sem.release(); }
}

六、生产验证#

  • Linux 内核semaphore.h#L15-L55struct semaphore 提供内核级计数信号量,down() 获取、up() 释放,用于设备驱动访问控制
  • Go 标准库semaphore.go#L28-L107Weighted 结构体支持加权获取,Acquire 阻塞直到权重可用或上下文取消
  • Java Semaphorejava.util.concurrent.Semaphore:支持公平(FIFO)和非公平两种模式,是 Java 并发编程的基础工具

七、小结#

何时使用:

  • 限流——限制并发 API 调用、数据库连接数
  • 资源池——控制对固定数量资源的访问
  • 背压——防止压垮下游服务
  • 节流——限制并发文件 I/O、网络请求

何时不用:

  • 互斥——如果需要独占访问(max=1),用 mutex 更安全
  • 简单计数——不需要阻塞就用原子计数器
  • 顺序敏感——如果处理顺序重要,用有界队列代替
  • 资源复用——信号量只管并发数,不管资源生命周期;需要连接池而非裸信号量

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

信号量(Semaphore)
https://blog.souloss.com/posts/programming/concurrency/concurrency-semaphore/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时