【Linux】POSIX信号量

一、信号量
POSIX版本的信号量(可用于线程),如果要用于进程可以强转,比如把共享资源的前面一部分内容直接强转成POSIX版本的信号量(相当于顶替定义操作了)

信号是一个计数器,用来描述一大片共享资源中可用资源的数量(我们把这一大片共享资源给划分了)
信号量的本质是对资源的预订机制
线程要获得资源,要先 P 操作 (等待操作,计数器--,申请信号量)
使用完资源以后,要 V 操作 (发布操作,计数器++,释放信号量)
P 操作和 V 操作本身是原子的
如果当前信号量不够了(P的时候,信号量为0),则该线程会被加入到信号量的等待队列中。(直到有线程 V 操作把信号量给放出来了)
二、POSIX信号量接口
1. 初始化和销毁
初始化

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

sem_t信号量的类型
sem:要初始化的信号量
pshared:零表示线程间共享,非零表示进程间共享(传0就行)
value:信号量初始值
返回值:成功,零,失败,非零
销毁

int sem_destroy(sem_t *sem);

2. 等待和发布
等待

int sem_wait(sem_t *sem); // P 操作

当信号量为0,线程就会被放到信号量等待队列里面等
这个操作是原子的
如果为负数就不能访问临界资源,也就是说,在访问临界资源之前,信号量已经对“条件”进行了判断(相当于互斥锁的while)
发布

int sem_post(sem_t *sem); // V 操作
1
三、基于环形队列的生产者消费者模型

1. 基本实现
1.1 思路
问题描述

生产者往空的位置上生产,消费者消费不为空的位置。
队列为空:生产者先生产(消费者不能超过生产者)
队列为满:消费者先消费(生产者不能套消费者一圈以上)
问题抽象

生产者只关注空盘子
消费者只关注非空盘子
两者在同一位置时(为空 / 为满),两者才需要同步和互斥
不在同一位置时,生产者和消费者之间行为独立
模型特点

3 个关系(基本不变)
生产与生产:互斥(单生产单消费时退化,只有一个生产者就没有互斥)
消费与消费:互斥
生产与消费
不在同一位置时:互不影响(无关系)
在同一个位置时:互斥 + 同步
2 个角色:生产者和消费者线程
1 个交易场所(环形队列)
实现

用二元信号量(且这两个信号量之间有关联)
一个描述空盘子数量
一个描述非空盘子的数量
用vector来模拟环形队列,生产和消费往vector对应下标里进行
1.2 实现及运行
实现
代码实现

RingQueue.hpp文件:

#include <semaphore.h>
#include <vector>
#include <string>
#include <pthread.h>

using namespace std;

template <typename T>
class RingQueue
{
public:
RingQueue(int size)
:_cap(size),
_p_step(0),
_c_step(0)
{
_ringqueue.resize(size);
sem_init(&_empty_sem, 0, size);
sem_init(&_noempty_sem, 0, 0); // 初始时,没有非空位置
pthread_mutex_init(&_mutex_p, nullptr);
pthread_mutex_init(&_mutex_c, nullptr);
}

~RingQueue()
{
sem_destroy(&_empty_sem);
sem_destroy(&_noempty_sem);
pthread_mutex_destroy(&_mutex_p);
pthread_mutex_destroy(&_mutex_c);
}

void Push(const T &data)
{
// 1. 申请信号量
sem_wait(&_empty_sem); // 如果失败就阻塞
// 多生产之间 要加锁
pthread_mutex_lock(&_mutex_p);
// 2. 生产
_ringqueue[_p_step] = data;
// 3. 生产者位置改变(并维护环形队列)
_p_step = (_p_step + 1) % _cap;
// 4. 改变 _noempty_sem,通知消费者
sem_post(&_noempty_sem);
// 解锁
pthread_mutex_unlock(&_mutex_p);
}
void Pop(T* data)
{
// 1. 申请信号量
sem_wait(&_noempty_sem); // 如果失败就阻塞
// 多消费者之间 要加锁
pthread_mutex_lock(&_mutex_c);
// 2. 消费
*data = _ringqueue[_c_step];
// 3. 消费者位置改变(并维护环形队列)
_c_step = (_c_step + 1) % _cap;
// 4. 改变 _noempty_sem,通知生产者
sem_post(&_empty_sem);
// 解锁
pthread_mutex_unlock(&_mutex_c);
}

private:
vector<T> _ringqueue;
int _cap;
sem_t _empty_sem; // 描述空位置
sem_t _noempty_sem;

int _p_step; // 生产者所在位置
int _c_step;

// 多生产,多消费需要加锁,维护生产者和生产者,消费者和消费者之间的互斥关系
pthread_mutex_t _mutex_p;
pthread_mutex_t _mutex_c;
};

Main.cpp文件

#include "RingQueue.hpp"
#include <iostream>
#include <unistd.h>

int num = 1;

void* Pro(void* args)
{
RingQueue<int>* p = static_cast<RingQueue<int>*>(args);
while(true)
{
// sleep(2);
p->Push(num);
cout << "生产了一个任务: " << num <<endl;
num++;
}
}

void* Com(void* args)
{
RingQueue<int>* p = static_cast<RingQueue<int>*>(args);
while(true)
{
sleep(2);
int ret;
p->Pop(&ret);
cout << "消费了一个任务: " << ret << endl;
}
}

int main()
{
int num = 0;
RingQueue<int> super(5);
// 单生产,单消费
// pthread_t p[1], c[1];

// pthread_create(&p[0], nullptr, Pro, &super);
// pthread_create(&c[0], nullptr, Com, &super);

// pthread_join(p[0], nullptr);
// pthread_join(c[0], nullptr);

// 多多
pthread_t p[3], c[2];

pthread_create(&p[0], nullptr, Pro, &super);
pthread_create(&p[1], nullptr, Pro, &super);
pthread_create(&p[2], nullptr, Pro, &super);
pthread_create(&c[0], nullptr, Com, &super);
pthread_create(&c[1], nullptr, Com, &super);

pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
}

运行
运行结果:
单生产单消费,且消费更快,则:生产一个消费一个

cout输出到屏幕上也有并发问题。

多生产,多消费(生产慢,消费快):

对全局变量num的++操作不是原子的,也会有并发问题。
但是我们基本可以看出来消费的都是旧任务,生产的是新任务。

2. 细节解刨
2.1 多多模型的加锁
多多模型相比单单模型,缺少的就是3个关系中的前两个
多多模型,加锁的位置

在申请信号量之前和之后加都可以,但是在之后加效率更高
因为申请信号量也是原子操作,多个线程可以先分配好信号量,然后再去竞争锁。
如果先申请锁的话,那么每次只能把信号量分给一个线程。
2.2 对比互斥锁 + 条件变量实现的模型
互斥锁 + 条件变量

互斥锁 + 条件变量的实现,用在当资源作为整体使用的时候
我们使用queue作为交易场所,但是queue不能同时往多个地方入队,所以只能整体使用
信号量

而信号量可以用于:当资源整体能被划分成多个小资源,且小资源可以同时访问的场景
如果资源数量size为 1,则这时候也就退化成了互斥锁 + 条件变量模型
进一步封装

当然我们也可以把库的信号量 / 锁封装起来,让代码更优雅
然后把访问临界区的代码用{}扩起来,在{}内定义的变量声明周期随{}
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/tan_run/article/details/148363140

阅读剩余
THE END