135 / 163 · C11 · 约 8 分钟
信号量
先记住这句话
信号量用一个整数计数器加上阻塞与唤醒原语来协调线程。初始化值决定它充当互斥锁还是事件通知器。本章用独立示例说明 wait/post 语义、二元用法以及父子线程排序,并给出可编译的 C 演示。
计数器模型与两种操作
信号量保存一个整数。wait 把该整数减一,若结果变成负数则调用者进入睡眠队列。post 把整数加一,若队列非空则唤醒其中一个等待者。负值的绝对值恰好等于当前睡眠线程数。所有修改必须在原子临界区内完成,因此真正实现通常还需要一把底层锁。
把信号量当锁使用
把计数器设为 1 即可获得互斥。第一个 wait 成功并把计数器变成 0,后续 wait 会阻塞。对应的 post 把计数器加回 1 并可能唤醒一个竞争者。因为只有“持有/未持有”两种可见状态,这种用法常被叫做二元信号量。临界区越短越好,否则会降低并发度。
用信号量强制执行顺序
把计数器设为 0 可以让一个线程等待另一个线程的完成信号。子线程做完工作后执行 post,父线程在 wait 上阻塞直到该信号到达。此模式避免了忙等待,也比 condvar 更轻量,因为不需要额外的谓词检查。多个等待者时唤醒顺序由实现决定,通常是 FIFO。
常见误区
- 未调用 sem_init 就使用信号量属于未定义行为。
- 把锁信号量初始化成 0 会使第一个线程永远阻塞。
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define N 1000
sem_t lock;
int shared = 0;
void *inc(void *arg) { (void)arg;
for (int i = 0; i < N; i++) {
sem_wait(&lock);
shared++;
sem_post(&lock);
}
return NULL;
}
int main(void) {
pthread_t t[2];
sem_init(&lock, 0, 1);
for (int i = 0; i < 2; i++) {
pthread_create(&t[i], NULL, inc, NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(t[i], NULL);
}
printf("%d\n", shared);
sem_destroy(&lock);
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-31-semaphores.c -o example && ./example预期结果
2000
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
要用信号量保护一段临界区,初始值必须是多少?若误设为 0 会出现什么现象?
查看参考答案
必须是 1。设为 0 时第一个 wait 就会把计数器变成 -1 并睡眠,临界区永远无法进入。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。