3. 信号量 Semaphore
信号量基础 (Semaphore Basics)
- 定义 (Definition): 一种抽象的整数计数器 (Integer Counter), 用于控制多线程对共享资源的访问.
- 核心操作 (Core Operations):
down()(或 P()/wait()): 原子性地减少计数器值; 若值为0则阻塞线程.up()(或 V()/signal()): 原子性地增加计数器值; 唤醒等待的线程.
- 类型 (Types):
- 二进制信号量 (Binary Semaphore): 值仅0或1, 初始化为 1 的情况下等效于锁 (Lock).
- 计数信号量 (Counting Semaphore): 值可为任意非负整数, 表示资源的可用数量.
信号量的应用场景 (Applications of Semaphores)
-
互斥 (Mutual Exclusion):
1
2
3
4semaphore sem(1);
sem.down();
// 临界区 (Critical Section)
sem.up(); -
顺序约束 (Ordering Constraints):
-
例如确保线程A先执行任务再启动线程B, 功能类似cv:
1
2
3
4
5
6
7semaphore sem(0);
// 线程A
taskA();
sem.up();
// 线程B
sem.down();
taskB();
-
生产者-消费者问题 (Producer-Consumer Problem)
-
场景 (Setup):
- 生产者 (Producer) 填充缓冲区, 消费者 (Consumer) 清空缓冲区.
- 缓冲区容量有限 (MAX), 需同步生产与消费速率.
-
信号量方案 (Semaphore Solution):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21semaphore mtx(1); // 互斥锁 (Mutual Exclusion)
semaphore fullSlots(0); // 已填充的槽数
semaphore emptySlots(MAX); // 空闲槽数
// 生产者
producer() {
emptySlots.down(); // 等待空闲槽
mtx.down();
// 生产数据到缓冲区
mtx.up();
fullSlots.up(); // 通知消费者
}
// 消费者
consumer() {
fullSlots.down(); // 等待已填充槽
mtx.down();
// 消费数据
mtx.up();
emptySlots.up(); // 通知生产者
} -
关键问题 (Key Issues):
- 操作顺序 (Order of Operations): 若先获取互斥锁 (
mtx.down()) 再等待资源 (emptySlots.down()), 则在减少资源之后本线程 sleep 而没有释放 mtx (忽略了 cv 条件中 release lock, put the thread onto waiting set, sleep 的三合一条件) - 信号量分离 (Separate Semaphores):
fullSlots和emptySlots需独立管理, 以处理不对称的等待条件.
- 操作顺序 (Order of Operations): 若先获取互斥锁 (
信号量与监视器的对比 (Semaphores vs. Monitors)
- 监视器 (Monitors):
- 使用锁 (Lock) 实现互斥.
- 使用条件变量 (Condition Variables) 实现顺序约束.
- 需显式管理条件判断 (如
while (condition)).
- 信号量 (Semaphores):
- 单一机制同时处理互斥和顺序约束.
- 更简洁但易出错 (如错误操作顺序导致死锁).
- 典型区别 (Key Differences):
- 信号量的历史性 (History):
up()操作会被记录, 后续down()可直接通过. 条件变量的无记忆性 (No Memory):signal()若在wait()前调用则无效. (见下一个小节例子) - semaphore down 不能位于 mutex 锁内部: 信号量的 down 操作会导致线程阻塞, 但是在 mutex 锁内部会导致死锁. (见上一个小节的关键问题部分)
- 信号量的历史性 (History):
自定义等待条件的实现 (Implementing Custom Waiting Conditions)
-
挑战 (Challenge): 信号量原生仅支持基于计数器值的等待 (
value == 0). -
解决方案 (Solution): 用信号量模拟条件变量:
-
等待队列 (Waiting Queue): 使用集合 (Set) 存储等待线程的私有信号量.
-
示例代码 (Example):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21semaphore cokeLock(1); // 互斥锁
set<sem*> waitingProducers; // 等待的生产者队列
set<sem*> waitingConsumers; // 等待的消费者队列
// 生产者
producer() {
cokeLock.down();
while (numCokes == MAX) {
sem* s = new sem(0);
waitingProducers.insert(s);
cokeLock.up();
s->down(); // 阻塞等待
cokeLock.down();
}
numCokes++;
if (!waitingConsumers.empty()) {
waitingConsumers.front()->up(); // 唤醒消费者
waitingConsumers.erase(...);
}
cokeLock.up();
}
-
[!Potential Problem] > 中断风险 (Interruption Risk): 在释放锁 (
cokeLock.up()) 和等待 (s->down()) 之间可能被中断.
由于 semaphore 具有记忆功能, 在这里发生中断转移到 customer 线程之后, customer
会购买一个 coke 从而导致对应的s->up(), 而导致返回之后这里的s->down()会立即返回, 且结果上没有逻辑错误.
核心总结 (Key Takeaways)
- 信号量通过原子计数器同步线程, 支持互斥和顺序约束.
- 生产者-消费者问题需分离资源计数与互斥锁.
- 信号量比监视器更灵活但更易出错, 需谨慎设计操作顺序.
- 自定义等待条件可通过信号量队列实现, 但需处理复杂性和潜在风险.
http://example.com/2025/02/18/wesley_knowledge_repo/os/3.%20%E4%BF%A1%E5%8F%B7%E9%87%8F%20Semaphore/
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
