信号量(Semaphore)是多线程同步的经典机制 ,核心用于资源计数 ,通过 P() (申请资源,计数-1)和 V() (释放资源,计数+1)操作实现线程间的同步与互斥,分为二元信号量 (计数为1,等价于互斥锁)和计数信号量(计数>1,控制多线程访问有限资源)。
下面以Linux下的POSIX信号量 ( sem_t )为例,分基础计数信号量和生产者-消费者模型两个场景讲解,包含完整代码和详细注释。
一、POSIX信号量核心函数(C语言)
| 函数 | 功能 |
|---|---|
| sem_init(sem_t *sem, int pshared, unsigned int value) | 初始化信号量: - pshared=0:线程间共享;pshared=1:进程间共享; - value:信号量初始计数(资源数) |
| sem_wait(sem_t *sem) | 等价于P()操作:计数>0则减1,否则阻塞线程 |
| sem_post(sem_t *sem) | 等价于V()操作:计数加1,唤醒阻塞的线程 |
| sem_destroy(sem_t *sem) | 销毁信号量,释放资源 |
| sem_trywait(sem_t *sem) | 非阻塞版P():计数>0则减1返回0,否则直接返回错误 |
二、示例1:二元信号量(模拟互斥锁)
实现两个线程对共享变量的安全累加,信号量初始值为1,等价于互斥锁,保证临界区互斥访问。
c
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
// 全局共享变量
int shared_num = 0;
// 定义信号量(二元信号量,初始值1)
sem_t sem;
// 线程函数:对共享变量累加10000次
void* add_func(void* arg) {
for (int i = 0; i < 10000; i++) {
// P操作:申请资源(获取锁)
sem_wait(&sem);
// 临界区:操作共享变量
shared_num++;
// V操作:释放资源(释放锁)
sem_post(&sem);
}
return NULL;
}
int main() {
// 初始化信号量:线程间共享,初始值1
sem_init(&sem, 0, 1);
pthread_t t1, t2;
// 创建两个线程
pthread_create(&t1, NULL, add_func, NULL);
pthread_create(&t2, NULL, add_func, NULL);
// 等待线程执行完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 输出结果:正确值为20000
printf("最终shared_num = %d\n", shared_num);
// 销毁信号量
sem_destroy(&sem);
return 0;
}
编译运行:
bash
gcc sem_mutex.c -o sem_mutex -lpthread
./sem_mutex
输出:最终shared_num = 20000
三、示例2:计数信号量(生产者-消费者模型)
这是信号量最经典的应用,用两个信号量分别控制空闲缓冲区 数和数据缓冲区数,实现生产者生产数据、消费者消费数据的同步。
核心设计:
- space_sem :空闲缓冲区信号量,初始值=队列容量(如5),表示可生产的位置数;
- data_sem :数据缓冲区信号量,初始值=0,表示可消费的数据数;
- 生产者:先 P(space_sem) 申请空闲位置,生产后 V(data_sem) 通知消费者;
- 消费者:先 P(data_sem) 申请数据,消费后 V(space_sem) 通知生产者。
c
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdlib.h>
#define CAPACITY 5 // 循环队列容量
int queue[CAPACITY]; // 共享队列
int p_idx = 0; // 生产者索引
int c_idx = 0; // 消费者索引
// 信号量定义
sem_t space_sem; // 空闲位置数,初始=CAPACITY
sem_t data_sem; // 数据数量,初始=0
pthread_mutex_t p_lock; // 生产者锁,保护p_idx
pthread_mutex_t c_lock; // 消费者锁,保护c_idx
// 生产者线程:生产数据
void* producer(void* arg) {
int data = 0;
while (1) {
// 1. 申请空闲位置
sem_wait(&space_sem);
// 2. 加锁保护生产者索引
pthread_mutex_lock(&p_lock);
queue[p_idx] = data++;
printf("生产者生产:%d,位置:%d\n", queue[p_idx], p_idx);
p_idx = (p_idx + 1) % CAPACITY; // 循环队列
pthread_mutex_unlock(&p_lock);
// 3. 释放数据信号量,通知消费者
sem_post(&data_sem);
sleep(1); // 模拟生产耗时
}
return NULL;
}
// 消费者线程:消费数据
void* consumer(void* arg) {
while (1) {
// 1. 申请数据
sem_wait(&data_sem);
// 2. 加锁保护消费者索引
pthread_mutex_lock(&c_lock);
int data = queue[c_idx];
printf("消费者消费:%d,位置:%d\n", data, c_idx);
c_idx = (c_idx + 1) % CAPACITY; // 循环队列
pthread_mutex_unlock(&c_lock);
// 3. 释放空闲位置信号量,通知生产者
sem_post(&space_sem);
sleep(2); // 模拟消费耗时
}
return NULL;
}
int main() {
// 初始化信号量
sem_init(&space_sem, 0, CAPACITY);
sem_init(&data_sem, 0, 0);
// 初始化互斥锁
pthread_mutex_init(&p_lock, NULL);
pthread_mutex_init(&c_lock, NULL);
pthread_t p_tid, c_tid;
// 创建生产者和消费者线程
pthread_create(&p_tid, NULL, producer, NULL);
pthread_create(&c_tid, NULL, consumer, NULL);
// 等待线程执行(实际会无限循环,可手动终止)
pthread_join(p_tid, NULL);
pthread_join(c_tid, NULL);
// 销毁资源(实际不会执行到)
sem_destroy(&space_sem);
sem_destroy(&data_sem);
pthread_mutex_destroy(&p_lock);
pthread_mutex_destroy(&c_lock);
return 0;
}
编译运行:
bash
gcc sem_pc.c -o sem_pc -lpthread
./sem_pc
输出示例:
生产者生产:0,位置:0
消费者消费:0,位置:0
生产者生产:1,位置:1
生产者生产:2,位置:2
消费者消费:1,位置:1
...
四、关键说明
- 信号量与互斥锁的配合:信号量解决资源计数/同步顺序 ,互斥锁解决共享变量(索引)的互斥访问,二者缺一不可;
- **计数信号量的灵活度:**初始值可根据资源数调整(如初始值3表示允许3个线程同时访问资源);
- P/V操作的原子性: sem_wait 和 sem_post 是原子操作,不会被线程调度器打断,保证信号量计数的正确性。