在C++多线程编程中,信号量(Semaphore) 是一种非常重要的同步原机制,主要用于控制同时访问特定资源的线程数量,或者用于线程间的事件通知。
C++对信号量的支持主要分为两个阶段:
C++20 及以后:标准库直接提供了 <semaphore> 头文件,包含标准的信号量类。
C++20 以前:没有直接的信号量类,需要使用 std::mutex 和 std::condition_variable 手动封装。
第一部分:C++20 标准信号量 (<semaphore>)
从 C++20 开始,标准库在 <semaphore> 头文件中提供了轻量级且高效的信号量实现。
1. 核心类
C++20 提供了两个主要的类:
std::counting_semaphore<LeastMaxValue> (计数信号量)
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 个线程并发使用)。这是计数信号量的典型场景。
第四部分:总结与最佳实践
首选 C++20 标准库:如果你能使用支持 C++20 的编译器(GCC 11+, Clang 11+, MSVC 19.28+),请直接使用 <semaphore>,性能最好且跨平台。
遗留系统使用 std::mutex + std::condition_variable:如果必须在旧标准下工作,使用第二部分提供的封装类。
避免使用原生的操作系统 API:尽量不要在 C++ 代码中混用 POSIX (sem_t, sem_wait) 或 Windows API (CreateSemaphore),这会降低代码的可移植性。
注意死锁:虽然信号量比互斥锁灵活,但也更容易导致逻辑错误(例如忘记 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 操作 - 阻塞)
cppint 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 (打开或创建)
cppsem_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 (关闭句柄)
cppint sem_close(sem_t *sem);作用:关闭当前进程对该命名信号量的引用。
注意 :这不会从系统中删除信号量,只是当前进程不玩了。
sem_unlink (彻底删除)
cppint 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