信号量的本质是一个计数器,而申请信号量就是对资源的预订
🚀 POSIX信号量 和 SystemV信号量 作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
POSIX 信号量有两种:
sem_open
和 sem_close
函数管理。sem_init
和 sem_destroy
函数管理。#include <semaphore.h>
① 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:把信号量的地址传进来
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
② 销毁信号量
int sem_destroy(sem_t *sem);
③ 等待信号量
int sem_wait(sem_t *sem); //P()
功能:等待信号量,会将信号量的值减1
--就是对信号量进行申请,如果申请成功,该函数就会立即返回,并且代码继续往后走。如果不成功,即信号量不足了,就会被阻塞在这里。
④ 发布信号量
int sem_post(sem_t *sem); //V()
功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
(1)命名信号量相关函数
函数 | 功能 |
---|---|
sem_open | 打开或创建一个命名信号量 |
sem_close | 关闭命名信号量 |
sem_unlink | 删除命名信号量 |
sem_wait | P 操作:减少信号量值,如果信号量值为 0,则阻塞 |
sem_trywait | 非阻塞的 P 操作,如果信号量值为 0,直接返回错误 |
sem_post | V 操作:增加信号量值,并唤醒阻塞的进程或线程 |
sem_getvalue | 获取当前信号量的值 |
(2)无名信号量相关函数
函数 | 功能 |
---|---|
sem_init | 初始化一个无名信号量 |
sem_destroy | 销毁一个无名信号量 |
sem_wait | 同命名信号量 |
sem_trywait | 同命名信号量 |
sem_post | 同命名信号量 |
sem_getvalue | 同命名信号量 |
1. 创建信号量:
sem_t *sem = sem_open("/my_semaphore", O_CREAT, 0644, 1);
"/my_semaphore" 是信号量的名字。
O_CREAT 表示创建信号量。
0644 是权限位。
1 是信号量的初始值。
2. P 操作:
sem_wait(sem);
3. V 操作:
sem_post(sem);
4. 关闭和删除信号量:
sem_close(sem); sem_unlink("/my_semaphore");
1. 初始化信号量:
sem_t sem; sem_init(&sem, 0, 1);
&sem 是信号量指针。
0 表示信号量用于线程间同步(1 表示进程间同步)。
1 是信号量的初始值。
2. P 操作:
sem_wait(&sem);
3. V 操作:
sem_post(&sem);
4. 销毁信号量:
sem_destroy(&sem);
post
或 wait
)。#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h> // O_CREAT
#include <unistd.h> // sleep
int main() {
sem_t *sem = sem_open("/my_semaphore", O_CREAT, 0644, 1);
if (sem == SEM_FAILED) {
perror("sem_open failed");
return 1;
}
printf("Waiting on semaphore...\n");
sem_wait(sem);
printf("Critical section\n");
sleep(2); // Simulate work
printf("Exiting critical section\n");
sem_post(sem);
sem_close(sem);
sem_unlink("/my_semaphore");
return 0;
}
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t sem;
void* thread_func(void* arg) {
sem_wait(&sem);
printf("Thread %d in critical section\n", *(int*)arg);
sleep(1); // Simulate work
printf("Thread %d exiting critical section\n", *(int*)arg);
sem_post(&sem);
return NULL;
}
int main() {
sem_init(&sem, 0, 1);
pthread_t t1, t2;
int id1 = 1, id2 = 2;
pthread_create(&t1, NULL, thread_func, &id1);
pthread_create(&t2, NULL, thread_func, &id2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sem);
return 0;
}
🔥 POSIX 信号量提供了一种简单而灵活的机制来解决多线程或多进程编程中的同步问题。通过选择合适的信号量类型和正确使用信号量操作,可以高效管理共享资源的访问。
🔥 POSIX 信号量 和 System V 信号量 是两种实现信号量的机制,都用于进程或线程间的同步,但它们在实现细节、功能和使用方式上存在显著差异
之前 System V 信号量我们在这篇博客里 【Linux】 IPC 进程间通信(三)(消息队列 & 信号量) 说过
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
标准来源 | POSIX 标准(IEEE) | System V IPC(UNIX 系统早期) |
灵活性 | 支持线程间和进程间同步 | 仅支持进程间同步 |
实现方式 | 提供命名信号量和无名信号量两种形式 | 通过内核中维护的信号量集实现 |
(1)信号量的类型与使用范围
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
命名信号量 | 支持,通过 sem_open 创建命名信号量 | 不支持 |
无名信号量 | 支持,通过 sem_init 初始化 | 不支持 |
线程间同步 | 支持,可以直接用于线程同步(如 pthread) | 不支持 |
进程间同步 | 支持(通过共享内存实现) | 支持 |
(2)信号量的创建与标识
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
标识方式 | 命名信号量通过名字标识,无名信号量通过变量标识 | 通过信号量集的 key 标识 |
创建函数 | sem_open(命名)或 sem_init(无名) | semget |
(3)操作方式
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
P 操作(等待) | sem_wait | semop(使用结构体数组描述操作) |
V 操作(释放) | sem_post | semop |
非阻塞操作 | 提供 sem_trywait | 无直接支持 |
获取信号量值 | sem_getvalue | 通过 semctl 查询 |
(4)信号量的创建与标识数
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
易用性 | 接口简单,易于学习和使用 | 接口复杂,使用时需要多个步骤 |
性能 | 较高,特别是无名信号量(内存中实现,无系统调用) | 较低,所有操作都涉及内核调用 |
(5)线程支持
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
线程间同步 | 支持,直接用于 pthread | 不支持,仅适用于进程间 |
(6)持久性
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
持久性 | 命名信号量在系统重启或进程退出后不持久 | 信号量集在系统重启后仍存在,需手动删除 |
删除方法 | sem_unlink(命名信号量) | semctl 的 IPC_RMID 标志删除信号量集 |
小结
特性 | POSIX 信号量 | System V 信号量 |
---|---|---|
适用场景 | 更适合现代多线程、多进程编程 | 更适合早期进程间通信 |
性能 | 较高,特别是无名信号量 | 较低,所有操作涉及内核 |
灵活性 | 支持线程同步,接口简单 | 仅支持进程同步,接口复杂 |
持久性 | 无持久性,信号量退出后即销毁 | 支持持久性,信号量需显式删除 |
🔥 总体而言,POSIX 信号量在现代系统中更常用,特别是在需要线程间同步时;System V 信号量则逐渐减少使用,但在某些传统 UNIX 环境中仍然可见
环形队列采用数组模拟,用模运算来模拟环状特性
🔥 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
实现思想:
生产者
int tail = 0;
P(空间)
// 给生产者空间,放入数据
ring[tail] = data;
tail++;
V(数据)
消费者
int head = 0;
P(数据)
// 给生产者空间,放入数据
int data = ring[head];
head++;
V(空间)
还有一些极端情况
为了尽量少的调用原生库,我们这里自己的封装POSIX信号量的C++类Sem
,主要功能是对信号量的初始化、销毁,以及提供P()
和V()
操作
Sem.hpp
#pragma once
#include <semaphore.h>
namespace SemModule
{
int defalutsemval = 1;
class Sem
{
public:
Sem(int value = defalutsemval):_init_value(1)
{
int n = ::sem_init(&_sem, 0, _init_value);
(void)n;
}
void P()
{
int n = ::sem_wait(&_sem);
(void)n;
}
void V()
{
int n = ::sem_post(&_sem);
(void)n;
}
~Sem()
{
int n = ::sem_destroy(&_sem);
(void)n;
}
private:
sem_t _sem;
int _init_value;
};
}
RingBuffer.hpp
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"
namespace RingBufferModule
{
using namespace SemModule;
template <typename T>
class RingBuffer
{
public:
RingBuffer(int cap): _ring(cap), _cap(cap), _p_step(0), _c_step(0), _datasem(0), _spacesem(cap)
{
}
// 这里我们为什么没有做判断 ?
// 原因:信号量,本身就是表示资源数目的,只要成功, 就一定会有, 不需要判断!! -> sem
// 之前 if 判断那里是把资源当作整体来申请,但是不会去整体使用(局部使用)
// 而且我们也不知道使用情况,所有需要在内部做判断
void Equeue(const T &in)
{
// 生产者
_spacesem.P();
_ring[_p_step] = in; // 生产完毕
_p_step++;
_p_step %= _cap; // 维持唤醒特性
_datasem.V();
}
void Pop(T *out)
{
// 消费者
_datasem.P();
*out = _ring[_c_step]; // 预订
_c_step++;
_c_step %= _cap;
_spacesem.V();
}
~RingBuffer()
{
}
private:
std::vector<T> _ring; // 环, 临界资源
int _cap; // 总容量
int _p_step; // 生产者位置
int _c_step; // 消费位置
Sem _datasem; // 数据信号量
Sem _spacesem; // 空间信号量
};
}
1. 类成员变量
2. 构造函数 RingBuffer(int cap) 与 析构函数 ~RingBuffer()
3. 生产者方法 Equeue(const T &in)
4. 消费者方法 Pop(T *out)
5. 信号量机制的设计
6. 为什么不需要判断资源是否充足
在信号量的使用中,sem_wait(P())会在资源不足时阻塞调用线程,而不是简单地返回错误。因此无需额外判断资源是否充足,信号量本身处理了资源的等待和唤醒
这个 RingBuffer 类是典型的生产者-消费者模式实现,使用了信号量来确保生产者和消费者之间的同步:
Main.cc -- 测试代码
#include "RingBuffer.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <functional>
using namespace RingBufferModule;
void *Consumer(void *args)
{
RingBuffer<int> *rb = static_cast<RingBuffer<int> *> (args);
while(true)
{
sleep(1);
// 1. 消费数据
int data;
rb->Pop(&data);
// 2. 处理: 花时间
std::cout << "消费了一个数据: " << data << std::endl;
}
}
void *Productor(void *args)
{
RingBuffer<int> *rb = static_cast<RingBuffer<int> *> (args);
int data = 0;
while(true)
{
// 1. 获取数据: 花时间
//sleep(1);
rb->Equeue(data);
// 2. 生产数据
std::cout << "生产了一个数据: " << data << std::endl;
data++;
}
}
int main()
{
RingBuffer<int> *rb = new RingBuffer<int>(5); // 共享资源-> 临界对象
// 单生产, 单消费
pthread_t c1, p1;
pthread_create(&c1, nullptr, Consumer, rb);
pthread_create(&p1, nullptr, Productor, rb);
pthread_join(c1, nullptr);
pthread_join(p1, nullptr);
delete rb;
return 0;
}
当然我们也可以进行多生产多消费如下:
int main()
{
RingBuffer<int> *rb = new RingBuffer<int>(5);
// 单生产, 单消费
pthread_t c1, p1, c2, p2, c3;
pthread_create(&c1, nullptr, Consumer, rb);
pthread_create(&p1, nullptr, Productor, rb);
pthread_create(&c2, nullptr, Consumer, rb);
pthread_create(&p2, nullptr, Productor, rb);
pthread_create(&c3, nullptr, Consumer, rb);
pthread_join(c1, nullptr);
pthread_join(p1, nullptr);
pthread_join(c2, nullptr);
pthread_join(p2, nullptr);
pthread_join(c3, nullptr);
delete rb;
return 0;
}
为了让我们的 RingBuffer.hpp 原生东西更少,我们调用之前实现的 Mutex.hpp,Mutex.hpp 具体可以看之前【多线程(互斥 && 同步)】博客,此时的 RingBuffer.hpp 代码如下:
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"
#include "Mutex.hpp"
namespace RingBufferModule
{
using namespace SemModule;
using namespace LockModule;
template <typename T>
class RingBuffer
{
public:
RingBuffer(int cap): _ring(cap), _cap(cap), _p_step(0), _c_step(0), _datasem(0), _spacesem(cap)
{
}
void Equeue(const T &in)
{
// 生产者
_spacesem.P();
{
LockGuard lockguard(_p_lock);
_ring[_p_step] = in; // 生产完毕
_p_step++;
_p_step %= _cap; // 维持唤醒特性
}
_datasem.V();
}
void Pop(T *out)
{
// 消费者
_datasem.P();
{
LockGuard lockguard(_c_lock);
*out = _ring[_c_step]; // 预订
_c_step++;
_c_step %= _cap;
}
_spacesem.V();
}
~RingBuffer()
{
}
private:
std::vector<T> _ring; // 环, 临界资源
int _cap; // 总容量
int _p_step; // 生产者位置
int _c_step; // 消费位置
Sem _datasem; // 数据信号量
Sem _spacesem; // 空间信号量
Mutex _p_lock;
Mutex _c_lock;
};
}
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂和发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !!