信号量(Semaphore)是操作系统提供的一种同步机制,由荷兰计算机科学家Dijkstra在1965年提出。它的本质是一个计数器,用于控制对共享资源的访问。在并发编程中,信号量主要解决两个核心问题:
信号量通过两个原子操作实现同步控制:
P操作(Proberen,荷兰语"尝试"):
V操作(Verhogen,荷兰语"增加"):
注意:PV操作必须是原子的,这意味着在执行过程中不能被中断,通常由操作系统内核提供保证。
根据初始值的不同,信号量可分为两类:
二元信号量(Binary Semaphore):
计数信号量(Counting Semaphore):
环形队列(Circular Queue)是一种首尾相连的线性数据结构,通过两个指针(生产指针和消费指针)实现循环访问。其核心特性包括:
使用环形队列的优势在于:
在生产者-消费者模型中,我们需要两类信号量:
空间信号量(space_sem):
数据信号量(data_sem):
这种设计确保了:
cpp复制// Sem.hpp
#pragma once
#include <semaphore.h>
class Sem {
public:
Sem(int num) : _num(num) {
sem_init(&_sem, 0, _num); // 第二个参数0表示线程间共享
}
void P() { // 申请资源
sem_wait(&_sem); // 原子操作,信号量-1
}
void V() { // 释放资源
sem_post(&_sem); // 原子操作,信号量+1
}
~Sem() {
sem_destroy(&_sem);
}
private:
sem_t _sem; // POSIX信号量
int _num; // 初始值
};
关键点说明:
sem_init的第二个参数为0表示线程间共享,若为1则表示进程间共享sem_wait和sem_post是POSIX标准提供的原子操作cpp复制// RingQueue.hpp
#pragma once
#include <vector>
#include "Sem.hpp"
const static int gcap = 5; // 默认容量
template<typename T>
class RingQueue {
public:
RingQueue(int cap = gcap)
: _cap(cap),
_ringqueue(cap),
_space_sem(cap), // 初始空间信号量=容量
_data_sem(0), // 初始数据信号量=0
_p_step(0), // 生产指针
_c_step(0) // 消费指针
{}
// 消费者从队列取出数据
void Pop(T* out) {
_data_sem.P(); // 申请数据资源
*out = _ringqueue[_c_step++];
_c_step %= _cap; // 环形处理
_space_sem.V(); // 释放空间资源
}
// 生产者向队列放入数据
void EnQueue(const T& in) {
_space_sem.P(); // 申请空间资源
_ringqueue[_p_step++] = in;
_p_step %= _cap; // 环形处理
_data_sem.V(); // 释放数据资源
}
~RingQueue() {}
private:
std::vector<T> _ringqueue; // 底层存储
int _cap; // 队列容量
Sem _space_sem; // 空间信号量
Sem _data_sem; // 数据信号量
int _p_step; // 生产位置指针
int _c_step; // 消费位置指针
};
cpp复制// main.cc
#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <iostream>
void* consumer(void* args) {
RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
while(true) {
sleep(1); // 模拟消费耗时
int data = 0;
rq->Pop(&data);
std::cout << "消费数据: " << data << std::endl;
}
}
void* productor(void* args) {
RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
int data = 1;
while(true) {
rq->EnQueue(data);
std::cout << "生产数据: " << data << std::endl;
data++;
usleep(500000); // 模拟生产耗时
}
}
int main() {
RingQueue<int>* rq = new RingQueue<int>();
pthread_t c, p;
pthread_create(&c, nullptr, consumer, rq);
pthread_create(&p, nullptr, productor, rq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete rq;
return 0;
}
虽然代码中没有显式使用互斥锁,但通过信号量的设计已经保证了线程安全:
生产者和消费者永远不会同时访问同一个位置
信号量的PV操作本身就是原子的
如果需要支持多个生产者和多个消费者,需要额外添加:
生产者互斥锁:
cpp复制pthread_mutex_t pmutex;
pthread_mutex_init(&pmutex, NULL);
// 在生产函数中
pthread_mutex_lock(&pmutex);
// 生产操作
pthread_mutex_unlock(&pmutex);
消费者互斥锁:
cpp复制pthread_mutex_t cmutex;
pthread_mutex_init(&cmutex, NULL);
// 在消费函数中
pthread_mutex_lock(&cmutex);
// 消费操作
pthread_mutex_unlock(&cmutex);
这样设计的原因是:
动态调整队列大小:
批量生产/消费:
无锁队列优化:
在实际项目中使用信号量和环形队列时,有几个容易踩坑的地方:
信号量初始值设置错误:
环形队列判满条件:
信号量泄漏:
性能瓶颈定位:
以下是一个改进版的信号量封装,增加了超时功能:
cpp复制class TimeoutSem {
public:
TimeoutSem(int num) : _num(num) {
sem_init(&_sem, 0, _num);
}
bool P(int timeout_ms) { // 带超时的P操作
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout_ms / 1000;
ts.tv_nsec += (timeout_ms % 1000) * 1000000;
return sem_timedwait(&_sem, &ts) == 0;
}
// ... 其他成员函数同上
};
这个改进可以避免线程永久阻塞,特别适合需要高可用性的场景。