详细介绍信号量

信号量(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

...

四、关键说明
  1. 信号量与互斥锁的配合:信号量解决资源计数/同步顺序 ,互斥锁解决共享变量(索引)的互斥访问,二者缺一不可;
  2. **计数信号量的灵活度:**初始值可根据资源数调整(如初始值3表示允许3个线程同时访问资源);
  3. P/V操作的原子性: sem_wait 和 sem_post 是原子操作,不会被线程调度器打断,保证信号量计数的正确性。
相关推荐
(Charon)2 小时前
【网络编程】从零开始理解 io_uring:Linux 网络编程的“核动力”引擎
linux·运维·服务器
历程里程碑2 小时前
Linux 10:make Makefile自动化编译实战指南及进度条解析
linux·运维·服务器·开发语言·c++·笔记·自动化
爱装代码的小瓶子2 小时前
【C++与Linux】文件篇(2)- 文件操作的系统接口详解
linux·c++
Cisco_hw_zte2 小时前
挂载大容量磁盘【Linux系统】
linux·运维·服务器
DolphinScheduler社区3 小时前
Linux 环境下,Apache DolphinScheduler 如何驱动 Flink 消费 Kafka 数据?
linux·flink·kafka·开源·apache·海豚调度·大数据工作流调度
艾莉丝努力练剑3 小时前
【AI时代的赋能与重构】当AI成为创作环境的一部分:机遇、挑战与应对路径
linux·c++·人工智能·python·ai·脉脉·ama
杜子不疼.3 小时前
【Linux】Ext系列文件系统(一):文件系统的初识
linux·运维·服务器
码农不惑3 小时前
systemd升级造成的centos-bootc系统的内核故障
linux·centos·bootc
若风的雨3 小时前
HIP Runtime资源分配相关的核心API分类总结
linux