POSIX信号量是什么呢?还记得在前面章节中我们介绍过System V 信号量,那这两个有什么关系和区别呢?
首先,信号量(Semaphore)是一种用于控制多个进程或线程对共享资源访问的同步机制。它由一个整数值和两个原子操作组成:
特性 | POSIX信号量 | System V信号量 |
|---|---|---|
标准来源 | IEEE POSIX标准(现代跨平台) | UNIX System V规范(传统系统) |
基本单元 | 单个非负整数 | 信号量集合(数组结构体) |
操作粒度 | 仅支持增减1(sem_wait/sem_post) | 支持任意增减值(semop可指定操作数) |
权限控制 | 不支持动态修改权限 | 可修改权限为原始权限的子集 |
初始化原子性 | 创建与初始化原子完成(sem_init) | 需分步创建(semget)和初始化(semctl) |
生命周期管理 | 无名信号量随进程结束自动清理 | 需显式删除(semctl(, IPC_RMID)) |
内存占用 | 轻量级(单信号量结构) | 高开销(支持最多25个信号量的集合) |
典型同步场景 | 线程间同步、简单进程同步(命名信号量) | 复杂进程间同步(如共享内存控制) |
POSIX信号量和SystemV信号量确实在功能上相似,都是用于进程或线程间的同步机制,确保对共享资源的无冲突访问。然而:
sem_initint sem_init(sem_t *sem, int pshared, unsigned int value);参数说明:
sem:指向要初始化的信号量对象的指针
pshared:
0:信号量在线程间共享(同一进程内的线程)
0:信号量在进程间共享(需要位于共享内存中)
value:信号量的初始值(通常表示可用资源数量)
返回值:成功返回0,失败返回-1并设置errno
sem_destroyint sem_destroy(sem_t *sem);参数说明:
sem:要销毁的信号量
返回值:成功返回0,失败返回-1并设置errno
重要注意事项:
sem_init配对使用
sem_waitint sem_wait(sem_t *sem);功能:执行P操作(等待/获取信号量)
返回值:成功返回0,失败返回-1并设置errno
sem_postint sem_post(sem_t *sem);功能:执行V操作(释放/发布信号量)
返回值:成功返回0,失败返回-1并设置errno
除了基本操作外,POSIX信号量还提供了一些有用的函数:
sem_trywaitint sem_trywait(sem_t *sem);功能:尝试获取信号量,如果信号量值为0,立即返回错误而不是阻塞
返回值:成功返回0,如果信号量值为0返回-1并设置errno为EAGAIN
sem_timedwaitint sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);功能:尝试获取信号量,但在指定的绝对时间前超时
参数:
sem:信号量
abs_timeout:绝对超时时间
返回值:成功返回0,超时返回-1并设置errno为ETIMEDOUT
sem_getvalueint sem_getvalue(sem_t *sem, int *sval);功能:获取信号量的当前值
参数:
sem:信号量
sval:输出参数,存储信号量的当前值
返回值:成功返回0,失败返回-1
注意:在多线程环境中,获取的值可能立即过时
和前面章节封装互斥量,条件变量一样,比较简单,不做讲解
代码如下:
#include <iostream>
#include <semaphore.h>
#include <pthread.h>
namespace SemModule
{
const int defaultvalue = 1;
class Sem
{
public:
Sem(unsigned int sem_value = defaultvalue)
{
int n = sem_init(&_sem, 0, sem_value);
if(n != 0)
{
perror("sem init failed");
}
}
void P()
{
int n = sem_wait(&_sem);
if(n != 0)
{
perror("sem wait failed");
}
}
void V()
{
int n = sem_post(&_sem);
if(n != 0)
{
perror("sem post failed");
}
}
~Sem()
{
int n = sem_destroy(&_sem);
if(n != 0)
{
perror("sem destroy failed");
}
}
private:
sem_t _sem;
};
}环形队列采用数组模拟,用模运算来模拟环状特性
使用固定大小的数组作为底层存储结构
通过模运算(index % capacity)实现循环访问
示例:当队尾指针到达数组末尾时,通过取模运算回到数组开头
rear = (rear + 1) % capacity;
环形结构的状态判断方案
方案 | 优点 | 缺点 |
|---|---|---|
计数器 | 判断简单 | 需要额外维护变量 |
标记位 | 实现直接 | 状态切换复杂 |
预留空间 | 逻辑清晰 | 浪费一个存储位 |

信号量在多线程同步中的应用
说白了,信号量就是一个计数器,我们只需要使用一个信号量记录空位的数量,再使用一个信号量记录数据的数量,入队,空位减1数据加1,出队,空位加1数据减1,队列满了就阻塞生产者,队列空了就阻塞消费者,队列不为空或者不为满,生产者生产数据,消费者消费数据两者可以同时进行。
下面我们同样还是先以单生产者单消费者为例,后面再改成多生产多消费
#pragma once
#include <iostream>
#include <vector>
#include "Sem.hpp"
using namespace SemModule;
static const int gcap = 5;
template <class T>
class RingQueue
{
public:
Ringqueue(int cap = gcap)
:_cap(cap)
,_rq(cap)
,_blank_sem(cap)
,_p_step(0)
,_data_sem(0)
,_c_step(0)
{}
~RingQueue() {}
private:
std::vector<T> _rq;
int _cap;
// 生产者
Sem _blank_sem; // 空位置
int _p_step; // 下一个空位置的下标
// 消费者
Sem _data_sem; // 数据
int _c_step; // 下一个数据的下标
};这里我们使用上面封装好的信号量,注意,对于空位置的信号量需要初始化为容量大小,因为一开始队列为空,全是空位置
生产者生产数据:
// 生产者
void Enqueue(const T& in)
{
// 1. 空位置信号量大于0,信号量减1并返回;
// 空位置信号量等于0,则阻塞直到信号量大于0被唤醒
_blank_sem.P();
// 2. 生产数据
_rq[_p_step] = in;
// 3. 更新下标
++_p_step;
// 4. 维护环形特性
_p_step %= _cap;
// 5. 数据信号量加1
// 如果队列为空,数据信号量则为0,在阻塞等待,此时会唤醒数据信号量
_data_sem.V();
}消费者同理,代码如下:
// 消费者
void Pop(T* out)
{
// 1. 获取信号量
_data_sem.P();
// 2. 消费数据
*out = _rq[_c_step];
// 3. 更新下标
++_c_step;
// 4. 维护环形特性
_c_step %= _cap;
// 5. 释放信号量
_blank_sem.V();
}这里和阻塞队列一样,不做过多解释
#include "RingQueue.hpp"
#include <unistd.h>
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* producer(void* args)
{
int data = 1;
RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
while(true)
{
std::cout << "生产了一个数据: " << data << std::endl;
rq->Enqueue(data);
data++;
}
}
int main()
{
RingQueue<int>* rq = new RingQueue<int>();
// 构建生产和消费者
pthread_t c[1], p[1];
pthread_create(c, nullptr, consumer, rq);
pthread_create(p, nullptr, producer, rq);
pthread_join(c[0], nullptr);
pthread_join(p[0], nullptr);
return 0;
}运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadSync/Sem$ ./rq
生产了一个数据: 1
生产了一个数据: 2
生产了一个数据: 3
生产了一个数据: 4
生产了一个数据: 5
生产了一个数据: 6
消费了一个数据: 1
生产了一个数据: 7
消费了一个数据: 2
生产了一个数据: 8
消费了一个数据: 3
生产了一个数据: 9
消费了一个数据: 4
生产了一个数据: 10
消费了一个数据: 5
生产了一个数据: 11
^C可以看到我们让生产者先运行,消费者sleep上1秒,生产者很快就将队列生产满,然后消费者消费旧数据,生产者又继续生产新数据。
但这是单生产单消费,我们使用信号量完成了生产者和消费者之间的互斥和同步,不需要维护生产者与生产者之间的互斥关系,也不需要维护消费者与消费者之间的互斥关系,那如果是多生产多消费呢?那我们就需要维护生产者与生产者之间,消费者与消费者之间的互斥关系,怎么维护?答案是加锁。生产者与生产者之间需要一把锁,消费者与消费者之间需要一把锁。
private:
std::vector<T> _rq;
int _cap;
// 生产者
Sem _blank_sem; // 空位置
int _p_step; // 下一个空位置的下标
// 消费者
Sem _data_sem; // 数据
int _c_step; // 下一个数据的下标
// 维护多生产,多消费
Mutex _pmutex;
Mutex _cmutex;那加锁应该怎么加呢?是在获取信号量前加还是之后加?解锁呢?
// 生产者
void Enqueue(const T& in)
{
_pmutex.Lock(); // 是在信号量前加锁?
// 1. 空位置信号量大于0,信号量减1并返回;
// 空位置信号量等于0,则阻塞直到信号量大于0被唤醒
_blank_sem.P();
_pmutex.Lock(); // 还是在信号量之后加锁?
// 2. 生产数据
_rq[_p_step] = in;
// 3. 更新下标
++_p_step;
// 4. 维护环形特性
_p_step %= _cap;
// 5. 数据信号量加1
// 如果队列为空,数据信号量则为0,在阻塞等待,此时会唤醒数据信号量
_pmutex.Unlock(); // 解锁是在信号量前?
_data_sem.V();
_pmutex.Unlock(); // 还是在信号量之后?
}正确的加锁策略是:先获取信号量,再获取互斥锁
1. 信号量在锁之前获取
2. 锁保护具体的操作
_p_step和_rq[_p_step]的修改
_c_step和_rq[_c_step]的访问
3. 信号量在锁之后释放
为什么不能先加锁再获取信号量?
如果先加锁再获取信号量,会导致严重的性能问题甚至死锁
问题在于:
_blank_sem.P()
我们可以举一个例子来帮助理解为什么应该先等待信号量,再加锁
想象一个电影院有:
角色对应:
_blank_sem 信号量
_data_sem 信号量
_cmutex 消费者锁
_pmutex 生产者锁
错误的方式:先加锁再等待信号量
这就像:
正确的方式:先等待信号量再加锁
这就像:
消费者消费数据同样如此
完整代码:
#pragma once
#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"
using namespace MutexModule;
using namespace SemModule;
static const int gcap = 5;
template <class T>
class RingQueue
{
public:
RingQueue(int cap = gcap)
:_cap(cap)
,_rq(cap)
,_blank_sem(cap)
,_p_step(0)
,_data_sem(0)
,_c_step(0)
{}
// 生产者
void Enqueue(const T& in)
{
// 1. 空位置信号量大于0,信号量减1并返回;
// 空位置信号量等于0,则阻塞直到信号量大于0被唤醒
_blank_sem.P();
_pmutex.Lock();
// 2. 生产数据
_rq[_p_step] = in;
// 3. 更新下标
++_p_step;
// 4. 维护环形特性
_p_step %= _cap;
// 5. 数据信号量加1
// 如果队列为空,数据信号量则为0,在阻塞等待,此时会唤醒数据信号量
_pmutex.Unlock();
_data_sem.V();
}
// 消费者
void Pop(T* out)
{
// 1. 获取信号量
_data_sem.P();
_cmutex.Lock();
// 2. 消费数据
*out = _rq[_c_step];
// 3. 更新下标
++_c_step;
// 4. 维护环形特性
_c_step %= _cap;
// 5. 释放信号量
_cmutex.Unlock();
_blank_sem.V();
}
~RingQueue() {}
private:
std::vector<T> _rq;
int _cap;
// 生产者
Sem _blank_sem; // 空位置
int _p_step; // 下一个空位置的下标
// 消费者
Sem _data_sem; // 数据
int _c_step; // 下一个数据的下标
// 维护多生产,多消费
Mutex _pmutex;
Mutex _cmutex;
};这里就不再测试了
进一步理解信号量:
信号量的本质:
信号量不仅用于实现同步互斥,更关键的是它能以原子操作的方式,在访问临界资源之前就完成对资源状态(如“是否存在”“是否就绪”)的判断。这种预先判断避免了传统条件判断(如if)可能存在的竞态条件问题。
信号量与互斥锁(mutex)的适用场景: