【linux系统编程】POSIX信号量+基于环形队列的生产消费模型

POSIX信号量+基于环形队列的生产消费模型

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1.POSIX信号量

上篇文章最后我们基于BlockQueue生产者消费者模型写了代码,测试什么的都通过了。最后我们说代码还有一些不足的地方,由这些不足从而引入了我们接下来要学的信号量!

我们在看一看不足的地方
在这里插入图片描述
1.一个线程,在操作临界资源的时候,必须临界资源是满足条件的!
2.可是,公共资源是否满足生产或者消费条件,我们无法直接得知(我们不能事先得知(在没有访问之前无法得知))
3.只能先加锁,再检测,再操作,再解锁
为什么要先加锁呢?因为你要检测的本质也是在访问临界资源!

总而言之就是,因为我们在操作临界资源的时候,有可能不就绪,但是我们无法提前得知,所以只能先加锁,在检测,根据检测结果,决定下一步怎么走!这是我们刚才写代码的逻辑。

那有没有一种方法在实际操作的时候就把对应的资源情况得知呢?
有的,信号量!

关于信号量我们以前学过,今天我们真正的用起来。

首先回答什么是信号量

再说信号量之前在说一个东西
只要我们对资源进行整体加锁,就默认了我们对这个资源整体使用。这点要注意。
但是实际情况可能存在,一份公共资源,但是允许同时访问不同的区域!
就好比我们之前在信号量说过的电影院的例子,电影院在我们现实生活中是由我们共享的,它的工作模式是可以同时让我们去坐到自己的座位上,真正的竞争关系只在于访问同一个座位时存在冲突。

下面说说什么是信号量
a.信号量本质是一把计数器

之前说过电影院的例子是这样说的,每一个想进入电影院看电影的时候,是不是只有你屁股坐到电影院某个座位上这个座位才属于你,是这样的吗?并不是,只要我先买票,票买到了虽然人还没有坐到电影院的某个座位上,即使很长时间没有过去,但是我心里很清楚,只要我票买了这个座位就会给我预留着。所以我们对应申请信号量的动作,起始就相当于购买电影票的概念。也就是说未来想访问这一份公共资源或者公共资源的某一区域,所有线程都必须遵守先申请信号量!只有申请成功的线程,就一定能保证在这份公共资源里一定有一个位置给你预留着让你去访问。那一定有一个位置给你预留让你去访问怎么保证呢?这是由程序员编码保证不同的线程可以并发访问公共资源的不同区域!
什么意思呢?就是说我对一份公共资源进行划分,比如是数组,进来之前先申请信号量,申请成功的线程,程序员通过编码保证让不同的线程能够访问公共资源的不同区域。
因为信号量是一把计数器,它是衡量临界资源中资源数量多少的计数器
所以比如说电影院有100个座位,每一个人进入电影票之前你得先买票,而票最多100张,不管最后100个人最后怎么坐,至少我保证进入电影院的人不会超过100个,所以对100个人经过合理的规划让他们坐到不同的座位上,不就不会出现冲突吗,并且还会并发访问这个资源。
所以在真正访问临界资源前,先申请信号量这是第一点。

b.只要拥有信号量,就在未来一定能够拥有临界资源的一部分,所以申请信号量的本质:对临界资源中特定小块资源的 预定机制

这就对应去电影院买票看电影,你买到票了但你根本就没有去看电影,你很清楚只要我把电影院票买到了,我在未来想看的时候一定有这个座位给我留着。所以买票的本质是对座位的预定机制。

所以未来访问公共资源需要先申请信号量。
先申请信号量,信号量本质是一把计数器,这意味着什么?
是不是通过这两点,有可能我们在访问真正的临界之前,我们其实就可以提前知道临界资源的使用情况!!!

基于这一点就有可能解决以前那种先加锁在检测临界资源,而现在不用给我检测临界资源了,你给我检测信号量就行了,在检测信号量期间这一份公共资源不会被访问。

只要申请成功,就一定有你的资源!
只要申请失败,就说明条件不就绪,你只能等!

所以我们就可以将信号量的申请成功或失败,直接或间接判断资源就绪的情况,所以我们就不再判断了。

总结一些,其实就是我们发现之前的代码根本原因就是你根本就不知道这个临界资源中资源的使用情况所以你只能自己先加锁再检测,能生产就生产不能就把自己挂起,但今天每一个线程都必须先申请信号量,信号量是一把计数器是衡量临界资源数量多少的计数器,它也是对资源的预定机制,所以你先不要着急访问临界资源也不要着急判断,你直接去申请信号量,只要你申请成功这个资源就一定是就绪的你直接生产就行了,申请失败那对不起这个资源并不满足你只能等待,而不管成功还是失败你都没有去访问临界资源,所以通过这个的方式把公共资源的数目先暴露出来,来确定资源有没有就绪。这就是信号量!

什么是信号量:信号量的本质是一把计数器,它是用来对临界资源某一块资源的预定机制。

为什么要有信号量:在访问临界资源之前通过信号量的方式得知资源的使用情况。

按照我们这种说法
线程要访问临界资源中的某一区域 ---- 先申请信号量 ---- 所有人必须要先看到信号量 ---- 信号量本身必须是:公共资源

信号量是一把计数器,那它匹配的一定有 递增 or 递减
伪代码,假设sem_tsem=10;
sem- -(信号量- -的操作) ----> 申请资源 ----> 必须保证操作的原子性 ---->
P操作

sem++(信号量++的操作) ----> 归还资源 ----> 必须保证操作的原子性 ---->
V操作

信号量核心操作:PV原语

接下来看看信号量的基本使用接口(怎么办?)

初始化信号量

sem_t //信号量类型,这是由pthead库为我们维护的信号量

在这里插入图片描述
sem:定义的信号量
pshared:0表示线程间共享,非零表示进程间共享
value:该信号量计数器的初始值,这个值完全取决于临界资源数目的多少

销毁信号量

在这里插入图片描述

等待信号量

对信号量计数器减一,就是信号量对应匹配的P操作
在这里插入图片描述

发布信号量

表示资源使用完毕,可以归还资源了。将信号量值加1。对应信号量匹配的V操作
在这里插入图片描述

2.基于环形队列的生产消费模型

目前关于信号量我们学了50%,下面我们找一个场景写代码来帮我们更深层次的理解它。

在数据结构初阶我们学过环形队列,今天这里我们就不在关注它是如何实现的,而关注点在于基于环形队列的生产消费模型!
在这里插入图片描述
我们也知道环形队列在物理上是一个数组,可以用数组来实现。
在这里插入图片描述
关于环形队列就回顾到这里。

那环形队列在生产者消费者这里我们怎么用它。

单生产单消费为例
既然是一个环形队列,未来一定有个生产者在放数据,有个消费者在拿数据。最开始它们俩一定指向同一个位置。
在这里插入图片描述
对于生产者和消费者而言,它们俩在什么情况也会访问同一个位置呢?

  1. 空的时候
  2. 满的时候
  3. 其他情况,生产者和消费者,根本访问的就是不同的区域!

下面我们举个例子
今天我们想玩一个游戏,一张桌子,桌子有N个空的盘子
在这里插入图片描述
然后你和我手拉手到了这张桌子的旁边,我们走到这个桌子时,桌子上N个空盘子,
在这里插入图片描述

我们倆玩的是你追我跑的游戏,我不断的向盘子里放一个苹果,放一个之后立马就往后跑。你要直接把苹果从盘子上拿走然后你觉得不好吃,然后你继续追我。盘子里不能放两个或两个以上的苹果,你也不能从空盘子里假装拿到了苹果,我们在完追逐游戏的时候,怎么样保证游戏正常的运行呢?我们有如下几个原则!

  1. 你不能超过我
  2. 我不能把你套一个圈以上
  3. 我们俩什么时候,会站在一起?
    a . 盘子全为空
    b. 盘子上全都是苹果(满)
    c.其他情况,我们俩指向的是不同的位置!

盘子全为空我们俩站在一起让谁先运行呢?
我(生产者)
盘子全都是苹果我们俩又站在一起了这次让谁先运行呢?
你(消费者)

根据上面的例子我们有了一个结论:
在环形队列中,大部分情况,单生产者和单消费是可以并发执行的!
只有在满或者空的时候,才会有互斥与同步的问题!!

对应到我们刚刚的说的环形队列中,桌子以及一个个盘子就是我们的环形队列,我叫做生产者线程,你叫做消费者线程。生产者和消费者只有在空满的时候访问同一个位置,其他都访问的是不同区域,所以当然可以并发了。

所以为了完成环形队列生产消费问题,我们要做的核心工作是什么?
你—>消费者
我—>生产者

  1. 你不能超过我
  2. 我不能把你套一个圈以上
  3. 我们俩什么时候,会站在一起?

那如何保证上面的性质呢?
就用我们刚学的信号量!
信号量是用来衡量临界资源中资源数量的!

以前我们从来没有谈论过资源的问题。即使在阻塞队列中我们所说的资源就是这个队列,我们是把队列当做整体来使用的。因为我们加了锁所以我们认为队列是资源。但你在想想资源这个东西是不是各花入各眼。

1.对于生产者而言,看中的是什么?
队列中剩余的空间
2.对于消费者而言,看中的是什么?
放入队列中的数据

所以为了更好衡量生产者和消费者,我们给空间资源定义一个信号量,给数据资源定义一个信号量
有了这个信号量空间资源有多少剩余多少,数据资源有多少剩余多少,计数器给你确定,要生成和消费都先申请信号量,空间信号量只要申请成功环形队列一定有个位置给你生产,同样资源信号量只要申请成功环形队列一定有个数据给你消费。所以去访问就行了,至于访问那一个由我们程序员写代码来确定。

接下来写一些伪代码帮助理解
在这里插入图片描述
生产者你想进入环形队列,对不起先不要着急,你先进行P操作,申请信号量。
申请成功,你就可以继续向下运行。
申请失败,当前执行流,阻塞在申请处
在这里插入图片描述
申请成功并且把数据放到队列中之后,就要进行V操作,这里有一个问题生产者把数据生产到队列中,它要走了,那么它曾经申请的信号量在它生产结束之后归还了吗?答案是并没有!因为把数据生产到队列中你走了但你生产者生产的数据依旧在你曾经申请的资源空格里面放着,你人走了数据还在,数据还在格子就依旧被占用。 可是我还是做一下V操作啊,虽然我走了但是毕竟环形队列中多了一个数据,多了一个数据不就是多了一个消费者看中的资源吗,所以此时V不应该V的prodoctor_sem的信号量而是consumer_sem的信号量!
在这里插入图片描述
此时完成了一个生产过程。

对于消费者而言你首先也要进行P操作,申请信号量资源,你要申请的是消费者看中的数据资源。申请成功往下走从事消费活动。申请失败阻塞同样阻塞在申请处。
在这里插入图片描述
从事消费活动之后也要进行V操作,当消费者把队列中数据消费了走了数据已经不在了,那消费者申请的信号量数据有还吗?并没有!但是消费者把队列中数据拿走了,队列中的位置不就空出来一个吗。这意味着消费者消费一个数据那可以供我们生产的位置资源不就多了一个吗。所以消费者V的是prodoctor_sem
在这里插入图片描述

最开始环形队列是空的,生产者线程和消费者线程并发谁先运行呢?
答案是谁先运行不确定,但一定是谁申请成功谁往下运行!
当队列为空的是,一定是生产者申请成功继续往下执行代码。因为消费者最开始资源计数器为0只能是申请失败阻塞等待!当生产者执行完V操作之后,消费者consumer_sem一定由0->1。那消费者P操作一定成功继续往后运行。
所以当队列为空的时候,一定能保证只有一个执行流进来 ,一定只能保证生产者先运行!

假设极端场景,消费者就不消费,那么生产者能不能把消费者套一个圈,绕过消费者在继续生产呢?
不可以!因为信号量是一个计数器,生产者连续消费prodoctor_sem一直在减减,虽然consumer_sem一直在加加但是先不消费,一直生产生产最后prodoctor_sem减到0 ,它还想在生产还能申请到信号量吗?并不能!所以即便是消费者不消费,生产者随便生产,对不起你生产满了你在申请空间资源的时候你申请不到了,你就会阻塞挂起!所以生产者不能把消费者套一个圈

假设现在已经生产满了,
在这里插入图片描述
此时生产线程和消费线程谁先运行我们也不确定,但是生产一定先阻塞在申请处,消费申请资源成功继续往下运行。
所以满的时候生产和消费同时到来,生产一定申请不成功,进不到临界资源去访问。消费一定先运行!
生产者一直不生产,那有没有可能消费者把数据消费完了最终超过生产者呢?
并不会,消费完数据了已经没有数据可以消费了。消费者再去申请时只能阻塞挂起了!只能等生产者生产。所以消费者不能超过生产者

只有空满的时候我们俩会在同一个位置,你不能超过我,我不能把你套一个圈以上,除此之外生产者和消费者都是并发执行访问不同位置!

这些都是由信号量计数器来保证的!

未来,生产和消费的位置我们要想清楚。
1.其实就是队列中的下标
2.一定是两个下标
3.为空或者为满,下标相同

通过下标让生产和消费访问不同的位置,另外通过下标给我们的线程指派要访问的资源。

接下来我们写代码实现环形队列生产消费模型

上层调用逻辑大的框架

#include"ringqueue.hpp"
#include<ctime>
#include<sys/types.h>
#include<unistd.h>

void* productor(void* args)
{
   
    ringqueue<int>* rq=static_cast<ringqueue<int>*>(args);

    while(true)
    {
   
        
    }
}

void* consumer(void* args)
{
   
    ringqueue<int>* rq=static_cast<ringqueue<int>*>(args);

    while(true)
    {
   
        
    }
}

int main()
{
   
    //随机数种子,这里为了更随机
    srand((unsigned int)time(nullptr)^getpid());
    ringqueue<int>* rq=new ringqueue<int>();

    pthread_t p,c;
    pthread_create(&p,nullptr,productor,rq);
    pthread_create(&c,nullptr,consumer,rq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);

    return 0;
}

环形队列大的逻辑框架

#pragma once

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

using namespace std;

const int maxcapacity=5;

template<class T>
class ringqueue
{
   
public:
    ringqueue(int capacity=maxcapacity):_queue(capacity),_capacity(capacity)
    {
   
        //空间资源初始化为环形队列大小
        sem_init(&_spacesem,0,_capacity);
        //数据资源初始化为0
        sem_init(&_datasem,0,0);
        
        _pstep=_cstep=0;
    }
	
	//生产
    void push(const T&</
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值