【Linux开发】03Linux 线程同步:信号量(Semaphore)
一、问题:互斥量只能“锁”,不能“排队”
前面我们学习了互斥量,它可以解决多个线程同时访问共享资源的问题,保证同一时间只有一个线程进入临界区。但互斥量只能做到“互斥”,无法控制线程的执行顺序。
1.1 需要控制顺序的场景
假设有两个线程:
- 线程 A:读取用户输入的数字,存入全局变量
num。 - 线程 B:将
num累加到总和sum中。
我们希望:A 先输入 → B 再累加 → A 再输入 → B 再累加,如此交替 5 次。
如果用互斥量,只能保证 A 和 B 不会同时操作 num,但不能保证 A 先执行还是 B 先执行。可能出现 B 试图累加时,A 还没输入(num 还是旧值),导致错误。
1.2 需要一种能“同步顺序”的机制
我们需要一种工具,不仅能互斥访问,还能让线程按指定的顺序执行。比如:
- 初始时只允许 A 运行,B 等待。
- A 完成输入后,通知 B 可以运行,A 自己等待。
- B 累加后,通知 A 可以继续输入,B 等待。
这种机制就是信号量(Semaphore)。
二、什么是信号量?
2.1 概念:停车场的计数器
想象一个停车场,门口有一个电子牌显示剩余车位数量:
- 每进入一辆车,剩余车位减 1(P 操作)。
- 每离开一辆车,剩余车位加 1(V 操作)。
- 如果剩余车位为 0,后面来的车必须等待。
信号量就是一个整数计数器,它支持两种原子操作:
- P(等待):如果计数器 > 0,则减 1 并继续;否则阻塞等待。
- V(发信号):计数器加 1,并唤醒一个等待的线程(如果有)。
2.2 二进制信号量与计数信号量
- 二进制信号量:值只能是 0 或 1,相当于互斥量(但功能更强,可用于顺序控制)。
- 计数信号量:值可以大于 1,用于控制同时访问资源的线程数量(如连接池)。
2.3 与互斥量的区别
| 特性 | 互斥量 | 信号量 |
|---|---|---|
| 作用 | 互斥访问 | 互斥 + 顺序控制 |
| 值范围 | 0 或 1 | 0 ~ n |
| 谁可以解锁 | 只能加锁的线程解锁 | 任何线程都可以 V 操作 |
| 典型用途 | 保护临界区 | 生产者-消费者、顺序同步 |
三、信号量相关函数
信号量 API 与互斥量类似,但属于 POSIX 标准(头文件 <semaphore.h>)。
3.1 初始化:sem_init
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
- sem:指向信号量变量的指针。
- pshared:0 表示线程间使用(同一进程);非 0 表示进程间使用(需共享内存)。
- value:信号量的初始值。
- 返回值:成功返回 0,失败返回 -1。
3.2 等待(P 操作):sem_wait
int sem_wait(sem_t *sem);
- 如果信号量值 > 0,将其减 1 并立即返回。
- 如果信号量值 == 0,线程阻塞,直到值变为正数(被其他线程
sem_post)。
3.3 发信号(V 操作):sem_post
int sem_post(sem_t *sem);
- 将信号量值加 1。
- 如果有线程正在等待该信号量,则唤醒其中一个。
3.4 销毁:sem_destroy
int sem_destroy(sem_t *sem);
- 销毁信号量,释放资源。
四、完整示例:控制线程执行顺序
4.1 需求
- 线程 A:循环 5 次,每次从键盘输入一个数字,存入
num。 - 线程 B:循环 5 次,每次将
num累加到sum。 - 必须严格按照 A 输入 → B 累加 → A 输入 → B 累加 的顺序执行。
4.2 设计思路
使用两个信号量:
sem_one:初始值为 0,控制线程 B 是否可以累加。B 执行前sem_wait(&sem_one)(初始为 0,所以 B 阻塞),A 输入后sem_post(&sem_one)(值变为 1,B 被唤醒)。sem_two:初始值为 1,控制线程 A 是否可以输入。A 执行前sem_wait(&sem_two)(初始为 1,A 可以立即执行),输入后sem_post(&sem_one);A 再次循环时会sem_wait(&sem_two)等待 B 完成累加并sem_post(&sem_two)。
流程:
- A 先执行
sem_wait(&sem_two)(初始 1 → 0),输入数字。 - A 执行
sem_post(&sem_one)(0 → 1),唤醒 B。 - A 回到循环开始,执行
sem_wait(&sem_two)(此时值为 0,A 阻塞)。 - B 执行
sem_wait(&sem_one)(1 → 0),累加。 - B 执行
sem_post(&sem_two)(0 → 1),唤醒 A。 - 重复 5 次。
4.3 完整代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
static sem_t sem_one; // 控制 B 是否可以累加
static sem_t sem_two; // 控制 A 是否可以输入
static int num; // 共享变量
void *read(void *arg) {
for (int i = 0; i < 5; i++) {
// 等待许可(初始 sem_two 为 1,所以第一次直接通过)
sem_wait(&sem_two);
printf("Input num: ");
scanf("%d", &num);
// 通知 B 可以累加
sem_post(&sem_one);
}
return NULL;
}
void *accu(void *arg) {
int sum = 0;
for (int i = 0; i < 5; i++) {
// 等待 A 输入
sem_wait(&sem_one);
sum += num;
// 通知 A 可以输入下一个数字
sem_post(&sem_two);
}
printf("Result: %d\n", sum);
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化信号量
sem_init(&sem_one, 0, 0); // 初始为 0,B 一开始会阻塞
sem_init(&sem_two, 0, 1); // 初始为 1,A 可以立即执行
pthread_create(&t1, NULL, read, NULL);
pthread_create(&t2, NULL, accu, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sem_one);
sem_destroy(&sem_two);
return 0;
}
4.4 运行结果
Input num: 10
Input num: 20
Input num: 30
Input num: 40
Input num: 50
Result: 150
每次输入一个数,程序就会累加,不会出现 B 抢在 A 前面运行的情况。
五、信号量与互斥量的对比
| 场景 | 使用互斥量 | 使用信号量 |
|---|---|---|
| 保护共享数据 | ✅ 适合 | ✅ 也可(二进制信号量) |
| 控制执行顺序 | ❌ 无法实现 | ✅ 非常适合 |
| 允许多个线程同时访问(如连接池) | ❌ 只能一个 | ✅ 计数信号量 |
| 解锁者限制 | 必须加锁者解锁 | 任何线程都可以 V 操作 |
六、常见问题与注意事项
6.1 忘记初始化信号量
sem_t sem;
// 忘记 sem_init,直接使用会导致未定义行为。
6.2 信号量值溢出
sem_post 可能使信号量值超过初始设定的最大值(通常是 SEM_VALUE_MAX),但一般不会手动设置极大值。
6.3 死锁
如果两个线程互相等待对方 sem_post,且初始值设置不当,可能造成死锁。例如两个信号量初始都为 0,两个线程都先 sem_wait 对方,就会死锁。
6.4 进程间信号量
如果 pshared 非 0,信号量可用于不同进程(需放在共享内存中)。初学者先掌握线程间使用(pshared=0)。
6.5 编译链接
需要链接 pthread 库,同时也要链接 rt(某些系统需要 -lrt),但通常 -pthread 已包含:
gcc -pthread sem_example.c -o sem_example
七、总结
| 概念 | 说明 |
|---|---|
| 信号量 | 一个整数计数器,支持原子增减和阻塞等待 |
| P 操作(wait) | 值减 1,若值为 0 则阻塞 |
| V 操作(post) | 值加 1,唤醒等待线程 |
| 二进制信号量 | 值 0/1,类似互斥量 |
| 计数信号量 | 值 >1,控制并发数量 |
| 顺序控制 | 通过两个信号量可以实现线程交替执行 |
信号量使用四步曲
1. sem_init(&sem, 0, 初始值) // 初始化
2. sem_wait(&sem) // 等待(P)
3. sem_post(&sem) // 发信号(V)
4. sem_destroy(&sem) // 销毁
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)