线程同步之信号量:POSIX信号量的原理、sem_t的使用、信号量解决同步问题、信号量与条件变量的对比

各位同学,今天我们来聊聊多线程编程里一个非常实用的工具——信号量。说实话,我刚开始接触多线程时,总觉得同步问题很玄乎。后来在项目中踩过几次坑,才真正体会到信号量的价值。它不像互斥锁那样“非黑即白”,而是提供了一种更灵活的同步手段。

一、POSIX信号量的原理

信号量,说白了就是一个计数器。它用来控制对共享资源的访问数量。你可以把它想象成一个停车场门口的显示屏——显示还有多少个空车位。

POSIX信号量有两种:

  • 有名信号量:有名字,可以在不同进程间使用
  • 无名信号量:没有名字,通常用于线程间同步

我们课程主要讲无名信号量,因为线程同步用得最多。

信号量的核心操作只有两个:

  • sem_wait():把信号量减1。如果减完后是负数,线程就阻塞等待
  • sem_post():把信号量加1。如果有线程在等待,就唤醒一个

嗯,这里要注意:信号量的值永远不会小于0。当它为0时,再调用sem_wait()就会阻塞。这个机制非常巧妙,我当年第一次看到时,觉得这设计真简洁。

核心思想:信号量 = 计数器 + 等待队列

二、sem_t的使用

在C语言里,信号量类型是sem_t。使用前需要包含头文件<semaphore.h>

基本用法分三步:

  1. 初始化:调用sem_init()
  2. 操作:调用sem_wait()sem_post()
  3. 销毁:调用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个线程同时访问数据库,用信号量
  • 如果你要等待某个队列非空再处理,用条件变量

我个人习惯是:能用信号量解决的问题,尽量用信号量。因为它的接口更简单,不容易出错。但条件变量也有它的优势——可以精确控制唤醒哪个线程,或者唤醒所有线程。

经验之谈:我在做网络服务器时,用信号量控制并发连接数,用条件变量通知工作线程有新任务到来。两者配合使用,效果很好。

五、知识体系图

下面这张图总结了信号量的核心知识点,方便大家记忆:

POSIX信号量知识体系 信号量 sem_t 原理:计数器+等待队列 使用:init/wait/post/destroy 对比:vs 条件变量 sem_wait:减1,负则阻塞 sem_post:加1,唤醒等待者 值永远不会小于0 sem_init:初始化,指定初始值 sem_wait/sem_post:核心操作 sem_destroy:销毁,释放资源 信号量:自带计数器 条件变量:需配合互斥锁 信号量:自动唤醒一个 条件变量:signal/broadcast 适用场景:资源计数、限流、生产者-消费者

好了,信号量的内容就讲到这里。记住一句话:信号量是“有记忆”的同步工具,它知道还有多少资源可用。这个特性让它在很多场景下比条件变量更直接、更好用。


公众号:蓝海资料掘金营,微信deep3321