信号量(Semaphore)

在C++多线程编程中,信号量(Semaphore) 是一种非常重要的同步原机制,主要用于控制同时访问特定资源的线程数量,或者用于线程间的事件通知。

C++对信号量的支持主要分为两个阶段:

  1. C++20 及以后:标准库直接提供了 <semaphore> 头文件,包含标准的信号量类。

  2. C++20 以前:没有直接的信号量类,需要使用 std::mutex 和 std::condition_variable 手动封装。

第一部分:C++20 标准信号量 (<semaphore>)

从 C++20 开始,标准库在 <semaphore> 头文件中提供了轻量级且高效的信号量实现。

1. 核心类

C++20 提供了两个主要的类:

  1. std::counting_semaphore<LeastMaxValue> (计数信号量)

  2. std::binary_semaphore (二进制信号量)

这是最通用的信号量。它维护一个计数器。

  • 模板参数 LeastMaxValue:这是一个非负整数,表示信号量计数器的最大可能值。这允许编译器针对特定平台进行优化(例如,如果最大值很小,可以使用更小的内存类型)。

  • 构造函数 :接受一个整数,表示初始计数

这是 std::counting_semaphore<1> 的别名。它的计数器只有 0 和 1 两个状态。

  • 它常用于实现互斥(类似 std::mutex)或简单的"等待-通知"机制。

  • 与 std::mutex 的区别:std::mutex 有所有权概念(谁加锁谁解锁),而信号量没有(线程 A 可以 acquire,线程 B 可以 release)。

2. 主要成员函数

这两个类拥有相同的成员函数:

|---------------------------------|-------------------|-------------------------------------------|
| 函数名 | 作用 (PV操作) | 描述 |
| acquire() | P 操作 (Wait) | 阻塞当前线程,直到计数器 > 0。一旦成功,将计数器减 1。 |
| try_acquire() | 非阻塞 P 操作 | 尝试减少计数器。如果成功(计数器>0)返回 true,否则立即返回 false。 |
| try_acquire_for(rel_time) | 带超时 P 操作 | 阻塞直到成功或超过指定的时间段。 |
| try_acquire_until(abs_time) | 带超时 P 操作 | 阻塞直到成功或到达指定的时间点。 |
| release(ptrdiff_t n = 1) | V 操作 (Signal) | 将计数器增加 n(默认为 1),并唤醒等待的线程。 |

3. C++20 使用示例

场景:限制并发线程数(例如,只有 3 个停车位的停车场)。

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <semaphore> // C++20 头文件
#include <chrono>

// 定义一个计数信号量,最大计数至少为 3
// 初始值设为 3,表示一开始有 3 个资源可用
std::counting_semaphore<3> parkingLot(3);

void car(int id) {
    std::cout << "Car " << id << " is waiting for a spot...\n";
    
    // P操作:请求资源。如果计数为0,线程阻塞在这里
    parkingLot.acquire();
    
    std::cout << "Car " << id << " has parked! (Slots left: approximated)\n";
    
    // 模拟停车时间
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    std::cout << "Car " << id << " is leaving.\n";
    
    // V操作:释放资源。计数+1,唤醒其他等待线程
    parkingLot.release();
}

int main() {
    std::vector<std::thread> cars;
    // 模拟 6 辆车抢 3 个车位
    for (int i = 1; i <= 6; ++i) {
        cars.emplace_back(car, i);
    }

    for (auto& t : cars) {
        t.join();
    }
    return 0;
}

第二部分:C++20 以前的实现 (手动封装)


在 C++11/14/17 中,由于没有标准的 <semaphore>,通常的做法是结合 互斥锁 (std::mutex)条件变量 (std::condition_variable) 来实现。这是面试中常考的手写代码题。

1. 实现原理
  • 计数器:用 int count 维护资源数量。

  • 互斥锁:保护 count 变量的线程安全。

  • 条件变量:当 count == 0 时阻塞线程,当 count > 0 时唤醒线程。

2. 代码实现 (Semaphore 类)
cpp 复制代码
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <thread>
#include <vector>

class Semaphore {
private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;

public:
    // 构造函数:初始化资源数量
    explicit Semaphore(int initial_count = 0) : count(initial_count) {}

    // P操作 (acquire / wait)
    void wait() {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 使用 lambda 表达式防止虚假唤醒 (Spurious Wakeup)
        // 只有当 count > 0 时才继续,否则阻塞
        cv.wait(lock, [this]() { return count > 0; });
        
        count--; // 消耗资源
    }

    // V操作 (release / signal)
    void signal() {
        std::unique_lock<std::mutex> lock(mtx);
        count++; // 增加资源
        
        // 唤醒一个等待的线程。
        // 注意:通常先修改数据再 notify,lock 可以自动释放
        cv.notify_one(); 
    }
};

// --- 测试代码 ---
Semaphore sem(2); // 限制同时只有2个线程运行

void worker(int id) {
    std::cout << "Thread " << id << " waiting...\n";
    sem.wait(); // 获取信号量
    
    std::cout << "Thread " << id << " is working!\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    std::cout << "Thread " << id << " finished.\n";
    sem.signal(); // 释放信号量
}

int main() {
    std::vector<std::thread> threads;
    for(int i = 0; i < 5; ++i) {
        threads.emplace_back(worker, i);
    }
    
    for(auto& t : threads) t.join();
    return 0;
}

第三部分:信号量 vs 互斥锁 (std::mutex)

理解这两者的区别对于正确使用非常关键:

|----------|----------------------------|-------------------------------------------|
| 特性 | 互斥锁 (std::mutex) | 二进制信号量 (std::binary_semaphore) |
| 主要用途 | 保护共享数据(临界区)。 | 线程同步、事件通知。 |
| 所有权 | 有所有权。哪个线程加锁,必须由哪个线程解锁。 | 无所有权。线程 A 可以 acquire,线程 B 可以 release。 |
| 状态值 | 只有 锁定/未锁定。 | 0 或 1 (对于计数信号量则是 0 到 N)。 |
| 性能 | 通常经过高度优化,无竞争时极快。 | C++20 的实现也非常轻量,但语义略有不同。 |

计数信号量 (std::counting_semaphore) 的独特用途

互斥锁无法解决的问题,例如资源池管理(数据库连接池,有 10 个连接,允许 10 个线程并发使用)。这是计数信号量的典型场景。

第四部分:总结与最佳实践

  1. 首选 C++20 标准库:如果你能使用支持 C++20 的编译器(GCC 11+, Clang 11+, MSVC 19.28+),请直接使用 <semaphore>,性能最好且跨平台。

  2. 遗留系统使用 std::mutex + std::condition_variable:如果必须在旧标准下工作,使用第二部分提供的封装类。

  3. 避免使用原生的操作系统 API:尽量不要在 C++ 代码中混用 POSIX (sem_t, sem_wait) 或 Windows API (CreateSemaphore),这会降低代码的可移植性。

  4. 注意死锁:虽然信号量比互斥锁灵活,但也更容易导致逻辑错误(例如忘记 release,导致计数器永远不回升)。推荐使用 RAII 风格的封装(类似 std::lock_guard)来管理信号量的 acquire/release 对,但这需要自己编写包装器,因为标准库目前没有直接提供针对信号量的 RAII 守卫。

附:简单的 RAII 信号量守卫示例

cpp 复制代码
// 类似于 std::lock_guard,用于自动释放信号量
struct SemaphoreGuard {
    std::counting_semaphore<>& sem;
    
    SemaphoreGuard(std::counting_semaphore<>& s) : sem(s) {
        sem.acquire();
    }
    
    ~SemaphoreGuard() {
        sem.release();
    }
};

// 使用:
// {
//     SemaphoreGuard lock(parkingLot); 
//     // 做一些工作...
// } // 离开作用域自动 release

C语言<semaphore.h>

<semaphore.h> 是 POSIX 标准(Portable Operating System Interface)定义的信号量头文件。它是 C 语言在 Linux/Unix 系统下进行多线程或多进程同步的核心库。

在使用这个库之前,所有的函数都基于一个核心数据类型:
sem_t:这是一个不透明的数据类型(Opaque Type),你不需要(也不应该)知道它内部的具体结构,只需要通过指针操作它。

这个库的函数主要分为三类:操作类 (PV操作)、无名信号量生命周期管理命名信号量生命周期管理

|-----------------|----------------------|-------------------|
| 函数名 | 对应操作 | 典型使用场景 |
| sem_wait | P (Decrement/Lock) | 想要进入临界区,或等待资源可用。 |
| sem_post | V (Increment/Unlock) | 离开临界区,或通知资源已生产。 |
| sem_init | 构造 (内存版) | 多线程同步(最常用)。 |
| sem_destroy | 析构 (内存版) | 线程结束后清理。 |
| sem_open | 构造 (系统版) | 多进程同步(不同程序间)。 |
| sem_unlink | 删除 (系统版) | 彻底清除系统中的全局信号量。 |

以下是详细的函数签名及作用解释:


1. 核心操作函数 (PV 操作)

这些函数用于对信号量进行"等待(减)"和"发布(加)"操作。无论是有名信号量还是无名信号量,都使用这些函数进行控制。

sem_wait (P 操作 - 阻塞)

cpp 复制代码
int sem_wait(sem_t *sem);

作用:尝试将信号量的值减 1。

  • 如果当前信号量的值 > 0,则减 1 并立即返回。

  • 如果当前信号量的值 == 0,则阻塞当前线程,直到信号量变为 > 0(被其他线程 post)。

返回值:成功返回 0;失败返回 -1 并设置 errno。

sem_trywait (P 操作 - 非阻塞)

复制代码
int sem_trywait(sem_t *sem);

作用 :尝试将信号量的值减 1,但绝不阻塞

  • 如果信号量 > 0,减 1 并返回 0(成功)。

  • 如果信号量 == 0,立即返回 -1,并将 errno 设置为 EAGAIN(表示资源暂时不可用,请重试)。

场景:轮询或者做"如果拿不到锁我就去做别的事"的逻辑。

sem_timedwait (P 操作 - 带超时)

复制代码
#include <time.h>
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

作用 :尝试减 1,如果信号量为 0,则阻塞,直到成功或者超时

参数 :abs_timeout 是一个绝对时间点(例如:今天是2026年1月20日 20:30:00),而不是相对时间段(5秒后)。

返回值:如果超时仍未获取到,返回 -1,errno 设置为 ETIMEDOUT。

sem_post (V 操作 - 释放)

复制代码
int sem_post(sem_t *sem);

作用:将信号量的值加 1。

  • 如果有线程正在 sem_wait 上阻塞等待该信号量,其中一个线程会被唤醒。

特性 :它是异步信号安全(Async-Signal-Safe)的,这意味着你可以在信号处理函数(Signal Handler,如 SIGINT 处理函数)中安全地调用它。

sem_getvalue (获取当前值)

复制代码
int sem_getvalue(sem_t *sem, int *sval);

作用:获取信号量当前的计数值,存入 sval 指向的整数中。

注意:在多线程环境下,这个值仅供调试参考,因为返回的一瞬间值可能已经被其他线程改变了。

2. 无名信号量 (Unnamed Semaphores)

用于同一进程内的多线程 同步,或者父子进程/共享内存间同步。这类信号量直接存储在内存变量(sem_t)中。

sem_init (初始化)

复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);

作用:初始化一块内存中的信号量。

参数

  • sem:指向要初始化的信号量变量的指针。

  • pshared:共享范围标记

    • 0 :仅在当前进程的线程间共享(最常用)。

    • 非0 :在进程间共享(此时 sem 必须位于共享内存区域中)。

  • value:信号量的初始值(例如 1 代表互斥锁,N 代表资源池大小)。

返回值:成功 0,失败 -1。

sem_destroy (销毁)

复制代码
int sem_destroy(sem_t *sem);

作用:销毁信号量,释放相关资源。

注意:只能销毁处于"空闲"状态(没有线程在等待它)的信号量。销毁后不能再使用,除非重新 init。

3. 命名信号量 (Named Semaphores)

用于完全无关的进程之间通信。它们在系统中有一个类似文件名的名字(例如 /my_sem),即使进程关闭,信号量依然存在于内核中(直到系统重启或显式删除)。

sem_open (打开或创建)

cpp 复制代码
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

作用:打开一个现有的命名信号量,或者创建一个新的。

参数

  • name:信号量名字,通常以 / 开头,如 "/job_queue".

  • oflag:标志位。O_CREAT(不存在则创建),O_EXCL(如果已存在则报错)。

  • mode:权限位(类似文件权限,如 0644)。

  • value:如果创建新信号量,这是初始值。

返回值 :成功返回指向信号量的指针(注意这里返回指针,不是 int),失败返回 SEM_FAILED。

sem_close (关闭句柄)

cpp 复制代码
int sem_close(sem_t *sem);

作用:关闭当前进程对该命名信号量的引用。

注意 :这不会从系统中删除信号量,只是当前进程不玩了。

sem_unlink (彻底删除)

cpp 复制代码
int sem_unlink(const char *name);

作用 :从系统中移除该命名信号量。

机制:类似于文件的 unlink。如果还有进程打开了它,它不会立即消失,而是等到所有进程都 sem_close 后才真正销毁。

4. 代码示例 (无名信号量 - 多线程)

这是最常见的用法:

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t sem; // 定义信号量对象

void* worker(void* arg) {
    printf("Thread %ld waiting...\n", (long)arg);
    
    // P操作:请求资源
    sem_wait(&sem); 
    
    printf("Thread %ld obtained semaphore!\n", (long)arg);
    sleep(1); // 模拟工作
    printf("Thread %ld releasing...\n", (long)arg);
    
    // V操作:释放资源
    sem_post(&sem); 
    return NULL;
}

int main() {
    // 初始化:
    // 第二个参数 0 表示线程间共享
    // 第三个参数 2 表示同一时间允许 2 个线程访问
    if (sem_init(&sem, 0, 2) != 0) {
        perror("sem_init failed");
        return 1;
    }

    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, worker, (void*)1);
    pthread_create(&t2, NULL, worker, (void*)2);
    pthread_create(&t3, NULL, worker, (void*)3);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);

    // 销毁信号量
    sem_destroy(&sem);
    return 0;
}

编译提示

在使用 GCC/Clang 编译包含 <semaphore.h> 的代码时,通常需要链接 pthread 库:

gcc your_code.c -o output -lpthread

相关推荐
leaves falling2 小时前
c语言-动态内存管理
c语言·开发语言
Lution Young2 小时前
Qt隐式共享产生的问题
开发语言·qt
9稳2 小时前
基于单片机的家庭安全系统设计
开发语言·网络·数据库·单片机·嵌入式硬件
JQLvopkk2 小时前
C#调用Unity实现设备仿真开发浅述
开发语言·unity·c#
cheems95272 小时前
[Java EE]多线程模式下容器的选择
算法·哈希算法
每天吃饭的羊2 小时前
hash结构
开发语言·前端·javascript
一路往蓝-Anbo2 小时前
第37期:启动流程(二):C Runtime (CRT) 初始化与重定位
c语言·开发语言·网络·stm32·单片机·嵌入式硬件
Jackson@ML2 小时前
2026最新版Python 3.14.2安装使用指南
开发语言·python
橘子师兄2 小时前
C++AI大模型接入SDK—ChatSDK使用手册
开发语言·c++·人工智能