线程同步之信号量:POSIX信号量的原理、sem_t的使用、信号量解决同步问题、信号量与条件变量的对比
各位同学,今天我们来聊聊多线程编程里一个非常实用的工具——信号量。说实话,我刚开始接触多线程时,总觉得同步问题很玄乎。后来在项目中踩过几次坑,才真正体会到信号量的价值。它不像互斥锁那样“非黑即白”,而是提供了一种更灵活的同步手段。
一、POSIX信号量的原理
信号量,说白了就是一个计数器。它用来控制对共享资源的访问数量。你可以把它想象成一个停车场门口的显示屏——显示还有多少个空车位。
POSIX信号量有两种:
- 有名信号量:有名字,可以在不同进程间使用
- 无名信号量:没有名字,通常用于线程间同步
我们课程主要讲无名信号量,因为线程同步用得最多。
信号量的核心操作只有两个:
- sem_wait():把信号量减1。如果减完后是负数,线程就阻塞等待
- sem_post():把信号量加1。如果有线程在等待,就唤醒一个
嗯,这里要注意:信号量的值永远不会小于0。当它为0时,再调用sem_wait()就会阻塞。这个机制非常巧妙,我当年第一次看到时,觉得这设计真简洁。
核心思想:信号量 = 计数器 + 等待队列
二、sem_t的使用
在C语言里,信号量类型是sem_t。使用前需要包含头文件<semaphore.h>。
基本用法分三步:
- 初始化:调用
sem_init() - 操作:调用
sem_wait()和sem_post() - 销毁:调用
sem_destroy()
来看个简单的例子:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
void* worker(void* arg) {
// 等待信号量
sem_wait(&sem);
printf("线程 %ld 进入临界区\n", (long)arg);
// 模拟工作
sleep(1);
printf("线程 %ld 离开临界区\n", (long)arg);
// 释放信号量
sem_post(&sem);
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化信号量,初始值为1
sem_init(&sem, 0, 1);
pthread_create(&t1, NULL, worker, (void*)1);
pthread_create(&t2, NULL, worker, (void*)2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sem);
return 0;
}
这个例子中,信号量初始值为1,相当于一个互斥锁。同一时刻只有一个线程能进入临界区。
小技巧:sem_init()的第二个参数为0表示线程间共享,非0表示进程间共享。我一般只用0,跨进程同步的情况比较少。
三、信号量解决同步问题
信号量最擅长的,其实是解决“生产者-消费者”这类经典问题。我在做音视频处理项目时,就用信号量来协调编码线程和写入线程的工作节奏。
来看一个典型的生产者-消费者模型:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
sem_t empty; // 空槽位数量
sem_t full; // 已填充槽位数量
sem_t mutex; // 互斥锁
void* producer(void* arg) {
int item = 0;
while (1) {
item++;
// 等待空槽位
sem_wait(&empty);
// 加锁
sem_wait(&mutex);
// 生产数据
buffer[in] = item;
printf("生产者生产: %d 到位置 %d\n", item, in);
in = (in + 1) % BUFFER_SIZE;
// 解锁
sem_post(&mutex);
// 增加已填充槽位
sem_post(&full);
sleep(1);
}
return NULL;
}
void* consumer(void* arg) {
int item;
while (1) {
// 等待已填充槽位
sem_wait(&full);
// 加锁
sem_wait(&mutex);
// 消费数据
item = buffer[out];
printf("消费者消费: %d 从位置 %d\n", item, out);
out = (out + 1) % BUFFER_SIZE;
// 解锁
sem_post(&mutex);
// 增加空槽位
sem_post(&empty);
sleep(2);
}
return NULL;
}
int main() {
pthread_t prod, cons;
// 初始化信号量
sem_init(&empty, 0, BUFFER_SIZE); // 初始有5个空槽位
sem_init(&full, 0, 0); // 初始没有已填充槽位
sem_init(&mutex, 0, 1); // 互斥锁
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
sem_destroy(&empty);
sem_destroy(&full);
sem_destroy(&mutex);
return 0;
}
这个例子中,我们用三个信号量:
empty:记录缓冲区还有多少个空位full:记录缓冲区有多少个数据mutex:保护缓冲区的互斥访问
为什么需要三个?你想想看,如果只用互斥锁,生产者生产太快,消费者来不及消费,缓冲区就会溢出。信号量正好解决了这个“节奏控制”的问题。
避坑指南:我曾经在项目中把sem_wait()和sem_post()的顺序搞反了,结果导致死锁。记住:先检查资源(sem_wait),再操作共享数据,最后释放资源(sem_post)。这个顺序不能乱。
四、信号量与条件变量的对比
很多同学会问:信号量和条件变量(pthread_cond_t)到底有什么区别?我当年也困惑过。这里我给大家梳理一下:
| 对比维度 | 信号量 | 条件变量 |
|---|---|---|
| 本质 | 计数器 + 等待队列 | 等待队列 + 广播机制 |
| 使用场景 | 资源计数、限流 | 条件等待、状态变化通知 |
| 是否需要互斥锁配合 | 可选(但建议配合) | 必须配合互斥锁 |
| 唤醒方式 | 自动唤醒一个等待线程 | 可唤醒一个(signal)或全部(broadcast) |
| 是否携带状态 | 是(计数器记录资源数量) | 否(只负责通知,不记录状态) |
| 典型应用 | 生产者-消费者、限流 | 读写锁、线程池任务调度 |
说白了,信号量适合“资源有多少”的场景,条件变量适合“状态是否变化”的场景。
举个例子:
- 如果你要控制最多5个线程同时访问数据库,用信号量
- 如果你要等待某个队列非空再处理,用条件变量
我个人习惯是:能用信号量解决的问题,尽量用信号量。因为它的接口更简单,不容易出错。但条件变量也有它的优势——可以精确控制唤醒哪个线程,或者唤醒所有线程。
经验之谈:我在做网络服务器时,用信号量控制并发连接数,用条件变量通知工作线程有新任务到来。两者配合使用,效果很好。
五、知识体系图
下面这张图总结了信号量的核心知识点,方便大家记忆:
好了,信号量的内容就讲到这里。记住一句话:信号量是“有记忆”的同步工具,它知道还有多少资源可用。这个特性让它在很多场景下比条件变量更直接、更好用。