Skip to content

信号量

信号量是一种互斥的机制,相当于多把锁,可以理解为是加强版的互斥锁。

锁的相关知识可以参考线程部分。

为什么信号量是“加强版互斥锁”?

互斥锁(Mutex)本质是二元开关(0/1),仅保证独占访问;而信号量(Semaphore)是资源计数器(非负整数),支持更复杂的同步逻辑。关键差异如下:

特性互斥锁信号量
资源管理仅保护单个资源(如全局变量)可管理N个同类资源(如数据库连接池)
操作逻辑加锁/解锁必须由同一线程完成PV操作可跨线程(生产者释放,消费者获取)
适用场景临界区互斥(如写文件)资源调度、线程同步(如生产者-消费者模型)
本质关系-互斥锁 = 初始值为1的信号量特例

💡 核心加强点

  • 计数能力:信号量值 >1 时,允许多线程有限并发(如控制10个连接);
  • 解耦操作wait()(P操作)和 signal()(V操作)可跨线程,实现流程协同;
  • 同步扩展:通过初始值设计,既可模拟互斥锁(初值=1),也可实现线程唤醒链(初值=0)。

相关函数

定义信号量 sem_t sem;

c
int sem_init(sem_t *sem, int pshared, unsigned int value);

函数描述

  • 初始化信号量

函数参数

  • sem:信号量变量
  • pshared0表示线程同步,1表示进程同步
  • value:最多有几个线程操作共享数据

函数返回值

  • 成功返回0,失败返回-1,并设置errno值

c
int sem_wait(sem_t *sem);
  • 函数描述:调用该函数一次,相当于sem--,当sem0的时候,引起阻塞
  • 函数参数:信号量变量
  • 函数返回值:成功返回0,失败返回-1,并设置errno值

c
int sem_post(sem_t *sem);

函数描述

  • 调用一次,相当于sem++

函数参数

  • 信号量变量

函数返回值

  • 成功返回0,失败返回-1,并设置errno值

c
int sem_trywait(sem_t *sem);

函数描述

  • 尝试加锁,若失败直接返回,不阻塞

函数参数

  • 信号量变量

函数返回值

  • 成功返回0,失败返回-1,并设置errno值

c
int sem_destroy(sem_t *sem);

函数描述

  • 销毁信号量

函数参数

  • 信号量变量

函数返回值

  • 成功返回0,失败返回-1,并设置errno值

信号量代码片段

PV操作与底层原理

信号量的核心是原子操作PV,其行为由计数器(count)和等待队列决定:

cpp
// 简化版PV操作伪代码[6,7](@ref)
void P(Semaphore s) {  // wait()
    s.count--;
    if (s.count < 0) {
        将当前线程加入等待队列;
        阻塞线程;
    }
}

void V(Semaphore s) {  // signal()
    s.count++;
    if (s.count <= 0) {
        从等待队列移出一个线程;
        唤醒该线程;
    }
}
  • P操作:申请资源,若资源不足(count≤0)则阻塞;
  • V操作:释放资源,并唤醒等待队列中的线程。

⚠️ 关键设计原则

  1. 原子性保障:PV操作由系统/硬件保证不可中断,避免竞态;
  2. 等待策略:阻塞线程而非忙等待(busy-waiting),节省CPU资源;
  3. 公平性:可通过队列唤醒策略(如FIFO或优先级)避免饥饿。

实践场景与代码示例

互斥场景(替代Mutex)

初始化信号量值为1,实现临界区保护:

cpp
// C语言示例(POSIX信号量)
sem_t mutex;
sem_init(&mutex, 0, 1);  // 初值=1,模拟互斥锁

void thread_func() {
    sem_wait(&mutex);    // P操作
    // 临界区操作(如修改共享变量)
    sem_post(&mutex);    // V操作
}

资源池管理(如数据库连接池)

通过信号量限制最大并发数:

java
// Java Semaphore示例
Semaphore pool = new Semaphore(10);  // 允许10个并发连接

void queryDatabase() {
    pool.acquire();      // P操作:获取连接
    try {
        executeSQL();    // 使用数据库连接
    } finally {
        pool.release();  // V操作:释放连接
    }
}

生产者-消费者模型

双信号量协调空/满缓冲区:

cpp
// C++示例
sem_t empty, full;  
sem_init(&empty, 0, BUFFER_SIZE);  // 初始空槽数=缓冲区大小
sem_init(&full, 0, 0);             // 初始满槽数=0

void producer() {
    while (true) {
        item = produce_item();
        sem_wait(&empty);          // 等待空槽
        enqueue(item);
        sem_post(&full);           // 增加满槽计数
    }
}

void consumer() {
    while (true) {
        sem_wait(&full);           // 等待满槽
        item = dequeue();
        sem_post(&empty);          // 增加空槽计数
        consume(item);
    }
}

何时选择信号量

场景推荐机制原因
严格互斥(单资源独占)互斥锁开销更低,语义更直接
资源池/并发度控制信号量天然支持多资源计数
跨线程流程同步(如A完成后B)信号量(初值=0)解耦等待与通知

避坑指南

  • 避免将信号量误用为互斥锁(如忘记 post()导致死锁);
  • 优先使用高层同步工具(如条件变量+互斥锁组合),信号量更适用于资源计数场景。

通过理解计数器模型和PV原子操作,可灵活运用信号量解决复杂同步问题,其“加强”本质正在于资源抽象能力调度灵活性的统一。

system V版本

system V版本的信号量实现生产者消费者

cpp
#include <stdio.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    /** 创建共享内存
     * 第一个参数key设置为IPC_PRIVATE 将设置一个key为0的共享内存
     * 第二个参数为共享内存的大小,需要设置为内存页大小的整数倍,如果不满一页底层页将会按照一页进行设置
     * 第三个参数如果没有IPC_CREAT 没找到共享内存程序就会报错,如果有没找到就会创建一个
     *          | 0600 设置所创建的共享内存的权限为 600
     */
    int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
    if (shmid == -1) {
        perror("shmget");
        return -1;
    }

    /** 将共享内存映射到虚拟内存
     * 第一个参数为创建共享内存时返回的 id
     * 第二个参数需要填入一个虚拟内存的地址,填NULL可以让shmat自动分配
     * 第三个参数一般设置为 0
     */
    int *p = (int *) shmat(shmid, NULL, 0);
    if (p == (void *) -1) {
        perror("shmat");
        return -1;
    }

    // 定义生产者和消费者之间共享的资源
    p[0] = 10;// p[0] 表示空闲仓库的个数
    p[1] = 0; // p[1] 表示商品的个数

    /** 创建信号量
     * 第一个参数key为一个整数,但是要注意 如果这个key值已经被创建并设置过信号量程序会报错
     * 第二个参数为需要设置的信号量的个数,会为你创建一个数组,在设置信号量的时间需要填下标值
     * 第三个参数和创建共享内存时的第三个参数一致
     */
    int semid = semget(1000, 1, IPC_CREAT | 0600);
    if (semid == -1) {
        perror("semget");
        return -1;
    }

    /** 设置信号量
     * 第一个参数为创建的信号量的返回值
     * 第二个参数为需要设置的信号量的下标值
     * 第三个参数为需要进行的操作,设置信号量还需填入第四个参数
     * 第四个参数为要将信号量的值设置为多少
     */
    int ret = semctl(semid, 0, SETVAL, 1);
    if (ret == -1) {
        perror("semctl SETVAL");
        return -1;
    }

    // 获取设置的信号量
    ret = semctl(semid, 0, GETVAL);
    if (ret == -1) {
        perror("semctl GETVAL");
        return -1;
    }

    printf("semval = %d\n", ret);

    // 定义信号量PV操作的结构体
    struct sembuf P, V;
    P.sem_num = 0;//下标
    P.sem_op = -1;//对资源的影响
    P.sem_flg = SEM_UNDO;
    V.sem_num = 0;
    V.sem_op = 1;
    V.sem_flg = SEM_UNDO;


    if (fork() == 0) {
        // 子进程 生产者
        while (1) {
            // 在对共享内存进行操作之前加锁 P操作
            // 第三个参数为这个操作会影响到几个信号量
            semop(semid, &P, 1);
            if (p[0] > 0) {
                printf("before produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
                --p[0];
                ++p[1];
                printf("after produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
            }
            // 操作完共享内存之后解锁 V操作
            semop(semid, &V, 1);
        }
    } else if (fork() == 0) {
        // 子进程 第二个生产者
        while (1) {
            semop(semid, &P, 1);
            if (p[0] > 0) {
                printf("before produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
                --p[0];
                ++p[1];
                printf("after produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
            }
            semop(semid, &V, 1);
        }
    } else {
        // 父进程 消费者
        while (1) {
            semop(semid, &P, 1);
            if (p[1] > 0) {
                printf("before consume , space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
                --p[1];
                ++p[0];
                printf("after consume, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
            }
            semop(semid, &V, 1);
        }
        wait(NULL);
    }
    // 释放加载到虚拟内存的共享内存
    shmdt(p);
    // 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

SEM_UNDO可以在进程终止的时间将减去的资源加回来,从而来避免多线程访问共享资源而产生死锁的问题。