信号量:POSIX信号量,生产者消费者问题,哲学家就餐问题
说到信号量,我脑子里第一个蹦出来的画面,是十年前调试一个多线程数据采集系统时的场景。那时候板子上的四个传感器线程抢共享缓冲区,抢得乱七八糟,数据错得一塌糊涂。后来我用信号量重构了同步逻辑,问题迎刃而解。嗯,信号量这东西,说白了就是一把「令牌」,谁拿到谁干活,拿不到就等着。
POSIX信号量:两种形态
POSIX信号量分两种:命名信号量和匿名信号量。我个人习惯在进程间同步时用命名的,线程间同步时用匿名的。为什么呢?因为命名信号量有名字,不同进程可以通过名字找到同一个信号量;匿名信号量则直接放在共享内存里,轻量高效。
| 类型 | 创建函数 | 适用场景 |
|---|---|---|
| 命名信号量 | sem_open() |
多进程同步 |
| 匿名信号量 | sem_init() |
多线程同步 |
核心API就四个:sem_init、sem_wait、sem_post、sem_destroy。你想想看,这跟买票进站一个道理——sem_wait是检票,票少了就等着;sem_post是退票,票多了后面的人就能进。
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 1); // 初始值为1,相当于互斥锁
sem_wait(&sem); // P操作:申请资源
// 临界区代码
sem_post(&sem); // V操作:释放资源
sem_destroy(&sem);
生产者消费者问题:经典中的经典
生产者消费者问题,我面试时必问。为什么?因为它考察的是你对「同步」和「互斥」的理解深度。一个缓冲区,多个生产者往里放数据,多个消费者往外取数据。你想想看,如果不同步会怎样?生产者可能覆盖了还没被消费的数据,消费者可能读到半残的数据。
解决方案需要三个信号量:
- mutex:互斥锁,保护缓冲区访问,初始值1
- empty:空槽位计数,初始值等于缓冲区大小
- full:满槽位计数,初始值0
sem_t mutex, empty, full;
sem_init(&mutex, 0, 1);
sem_init(&empty, 0, BUFFER_SIZE);
sem_init(&full, 0, 0);
// 生产者
void producer() {
while (1) {
item = produce();
sem_wait(&empty); // 申请空槽
sem_wait(&mutex); // 进入临界区
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
sem_post(&mutex); // 离开临界区
sem_post(&full); // 增加满槽
}
}
// 消费者
void consumer() {
while (1) {
sem_wait(&full); // 申请满槽
sem_wait(&mutex); // 进入临界区
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
sem_post(&mutex); // 离开临界区
sem_post(&empty); // 增加空槽
consume(item);
}
}
哲学家就餐问题:死锁的教科书
哲学家就餐问题,说白了就是五个哲学家围着一张圆桌,每两人之间放一根筷子。哲学家要么思考,要么吃饭。吃饭需要拿起左右两边的筷子。如果所有哲学家同时拿起左边的筷子,那就死锁了——谁也没法吃饭。
这个问题我当年在课堂上听的时候觉得挺简单,直到自己在项目中遇到类似的资源竞争场景,才真正体会到它的精妙。嗯,这里的关键是破坏死锁的四个必要条件之一。
常见的解法有几种:
- 最多允许四个哲学家同时就餐:用一个计数信号量限制并发数
- 奇数哲学家先拿左边,偶数先拿右边:打破循环等待
- 用互斥锁保护拿筷子的动作:原子化操作
我个人最推荐第三种,因为它最直观,也最容易移植到其他场景。
sem_t chopstick[5];
sem_t mutex;
void philosopher(int i) {
while (1) {
think();
sem_wait(&mutex); // 保护拿筷子动作
sem_wait(&chopstick[i]); // 拿左边
sem_wait(&chopstick[(i+1)%5]); // 拿右边
sem_post(&mutex);
eat();
sem_post(&chopstick[i]);
sem_post(&chopstick[(i+1)%5]);
}
}
知识体系总览
下面这张图是我自己梳理的信号量知识脉络,你看一眼就能明白整个章节在讲什么。
实际项目中的选择建议
我在嵌入式项目中用过不少同步机制,信号量、互斥锁、条件变量都用过。给你一个实用的选择标准:
- 计数资源(比如有5个缓冲区可用)→ 用信号量
- 互斥访问(一次只能一个人用)→ 用互斥锁
- 等待特定条件(比如队列非空)→ 用条件变量
信号量特别适合「资源计数」的场景。比如一个串口DMA缓冲区有8个槽位,生产者放数据,消费者取数据,用信号量管理槽位数量,代码写起来干净利落。
好了,信号量这块就聊到这儿。你写代码的时候,多想想「谁在等什么资源」,信号量的值就是那个资源的数量。想通了这一点,生产者消费者和哲学家就餐问题,其实就是一个计数游戏。