信号量是多线程编程中用于控制并发访问共享资源的同步工具,核心逻辑是通过一个计数器记录可用资源数量,配合等待和释放操作实现线程的阻塞与唤醒。在C++标准库中并没有直接提供信号量实现,我们可以通过互斥锁和条件变量模拟计数器的变化,完成简单信号量的实现。

信号量的核心原理
信号量主要包含两个核心操作:
- wait(P操作):如果计数器大于0,将计数器减1并继续执行;如果计数器等于0,当前线程阻塞等待,直到有其他线程释放资源。
- post(V操作):将计数器加1,如果有阻塞的线程等待资源,唤醒其中一个线程。
实现信号量需要保证计数器的修改和线程的阻塞唤醒操作是线程安全的,因此需要使用互斥锁保护计数器的访问,用条件变量实现线程的等待和唤醒。
C++模拟信号量实现源码
下面是使用C++11标准库实现的简单信号量Semaphore类,包含构造、wait、post三个核心方法:
#include <mutex>
#include <condition_variable>
class Semaphore {
private:
int count; // 计数器,记录可用资源数量
std::mutex mtx; // 互斥锁,保护count的线程安全访问
std::condition_variable cv; // 条件变量,用于线程的等待和唤醒
public:
// 构造函数,初始化计数器为传入的初始值
explicit Semaphore(int init_count = 0) : count(init_count) {}
// wait操作,P操作
void wait() {
std::unique_lock<std::mutex> lock(mtx);
// 如果计数器为0,阻塞当前线程,等待条件变量唤醒
cv.wait(lock, [this]() { return count > 0; });
// 计数器减1,获取资源
--count;
}
// post操作,V操作
void post() {
std::unique_lock<std::mutex> lock(mtx);
// 计数器加1,释放资源
++count;
// 唤醒一个等待的线程
cv.notify_one();
}
};
代码逻辑解析
成员变量作用
count是信号量的核心计数器,记录当前可用的资源数量,初始值由构造函数传入,比如初始值为3表示最多允许3个线程同时访问共享资源。
mtx是互斥锁,所有对count的修改和读取操作都需要先获取该锁,避免多个线程同时修改计数器导致数据不一致。
cv是条件变量,当线程执行wait操作发现计数器为0时,通过cv.wait阻塞当前线程,释放互斥锁;当其他线程执行post操作增加计数器后,通过cv.notify_one唤醒一个阻塞的线程。
核心方法说明
wait方法中,先获取互斥锁,然后调用条件变量的wait方法,传入的lambda表达式作为 predicate,当predicate返回true时(即count大于0),线程不会被阻塞,直接执行后续逻辑将count减1;如果predicate返回false,线程会释放互斥锁并进入阻塞状态,直到被post方法唤醒。
post方法中,同样先获取互斥锁,将count加1后,调用notify_one唤醒一个等待的线程,被唤醒的线程会重新获取互斥锁,然后检查predicate,此时count已经大于0,因此会继续执行将count减1的逻辑。
使用示例
下面是使用上述Semaphore控制3个线程同时访问共享资源的示例:
#include <iostream>
#include <thread>
#include <vector>
// 共享资源访问函数
void access_resource(Semaphore& sem, int thread_id) {
sem.wait(); // 获取信号量,若资源不足则阻塞
std::cout << "线程" << thread_id << "获取到资源,开始访问" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟资源访问耗时
std::cout << "线程" << thread_id << "访问结束,释放资源" << std::endl;
sem.post(); // 释放信号量,唤醒等待的线程
}
int main() {
Semaphore sem(3); // 初始化信号量,允许3个线程同时访问
std::vector<std::thread> threads;
// 创建5个线程尝试访问资源
for (int i = 0; i < 5; ++i) {
threads.emplace_back(access_resource, std::ref(sem), i);
}
// 等待所有线程执行完成
for (auto& t : threads) {
t.join();
}
return 0;
}
上述示例中,信号量初始值为3,因此前3个线程会同时获取到资源,后续的2个线程会阻塞等待,直到前面的线程释放资源后被唤醒,实现了对共享资源的并发访问控制。
注意事项
- 信号量的初始值需要根据实际共享资源的数量设置,避免初始值过大导致资源访问冲突,或者过小导致线程长时间阻塞。
- wait和post操作需要成对出现,如果一个线程获取了信号量却没有释放,会导致其他等待的线程永远无法被唤醒,出现死锁问题。
- 上述实现是简单的信号量模拟,没有考虑信号量的析构、复制等场景,实际使用时可以根据需求补充相关逻辑,比如禁止复制信号量对象,避免多个对象操作同一个计数器导致逻辑错误。