引言:
北京时间:2023/8/2/12:52,时间飞逝,恍惚间已经来到了八月,给我的第一感觉就是快开学了,别的感觉其实没有,哈哈!看着身边的好友网络相关知识都要全部学完了,就好像它们工业革命都要完成了,而我还在刀耕火种,哈哈哈!他们在高速发展阶段,我还在休养生息,哈哈哈!不怕,正所谓没有压力没有动力,让压力来的再猛烈一些吧!咱无所畏惧,八月就是咱逆分翻盘之月,舍我其谁,冲冲冲!这句话我好想在哪见过,哈哈哈!别笑,我真的在很严肃的看待这件事。八月没有什么拓展内容,本来还想着在暑假的时候提高一下自己的做题能力,可惜,现在连课都学不明白,可能是当时无知以为自己时间很多,也可能确实是我们自己没把握好,反正现在不管那么多,八月我们的目标就是将网络搞定。废话不多说,进军网络之前,我们还是先把有关系统方面的知识先给搞定,今天就让我来看看基于信号量知识的生产消费模型吧!
基于环形队列的CP问题
上篇博客由于时间原因,这部分有关信号量接口实操的知识我们没有讲解,来到该篇博客,我们在重温生产消费模型的基础上,来看看如何实现信号量对共享资源中资源数量控制的同时,让多线程可以安全的访问共享资源吧!
1.复习环形队列
为什么使用环形队列?
在学习信号量时,我们重点强调了为什么要学习信号量,并且明白信号量分为二元信号量(互斥锁)和多元信号量,信号量就是一个对共享资源中资源数量的预定机制,结合当时我们没有使用信号量实现的生产消费模型我们可以发现,使用和不使用信号量的区别在于共享资源中是否存在多个临界资源需要被控制。在之前学习有关BlockQueue队列的CP模型我们可以发现,它并不存在多个临界资源,所以我们是直接采用互斥锁和条件变量的形式对其进行同步机制控制,也就是对那唯一的共享资源进行保护就行。而当我们今天想要实现一份有信号量知识的CP模型,那么前提就是需要有多个临界资源,而我们的环形队列就能很好的实现这一效果。
回顾环形队列相关的知识
明白了为什么要回顾环形队列之后,此时我们正式来复习一下环形队列,毕竟这块知识已经学习快一年了(去年11月份学的),并且当时出处茅庐,虽然也写了博客,但是当时的博客并没有非常用心,所以这一块知识给我的感觉比较模糊,该篇博客我们就来重点复习一下,由于这部分知识属于初阶数据结构,并不适合展开理解,也就是相关代码实现我们不体现出来,感兴趣的小伙伴可以参考我之前的代码实现,所以这里我们重点复习一下有关环形队列的概念知识,也就是环形队列的实现原理和特征。 我们都知道无论学习什么都是由浅入深,由概念到实操,如果一上来就是实操,那么你只会感觉天昏地暗,什么都听不懂,好比之前我们在学习生产消费模型,每个人的实操代码都有可能是不同的,但是其中的原理生产者和消费者的三种关系是谁都不能改变的,所有人都必须紧紧围绕这个原理来实现自己的CP模型。所以同理在数据结构中,无论是那种数据结构,它们本质的不同就在于设计原理不同,所以你想要看懂一份某数据结构的代码,前提就是你对该数据结构的设计原理了如指掌,同理你想实现一份代码,也是这个道理。现在就让我们来看看环形队列是由那些原理组成的吧!
环形队列原理和概念
环形队列也叫循环队列,本质是通过一个数组或者链表实现,同理只要按照环形队列的特征来实现,无论使用数组还是链表都没有太大区别,最本质的区别还要归咎于数组和链表的区别,也就是数组和链表各自的优缺点(这里我不展开复习),所以我们只要明白环形队列就是一个固定大小但可以重复使用的队列,其最大的特征在于开辟空间存储数据时,要多开辟一个空间(无论是数组还是链表实现),该空间用于解决环形队列最大的问题:判空判满问题,也就意味着我们在实现环形队列时,用户如果想要一个可以存储 K(10)个数据的环形队列,那么在环形队列内部new空间时就需要new K+1个空间,当然这一过程由于被封装的原因,上层用户并不能体会到,但是你下层代码为了实现这一功能,你就必须这么做(同理很多不看源码你无法理解的功能都是通过这种理念实现)。所以我们根据这一特征,此时就能明白如果环形队列是通过数组实现,那么此时它就需要通过两个整形数据front和tail作为数组下标来控制队列的循环过程,当(tail+1) % (K+1) = front
时,就表示此时环形队列满了,而如果我们的环形队列是通过链表来实现,那么此时它就需要通过两个结点指针来控制队列的循环过程,当tail->next = front
时表示环形队列满了。明白了这些知识之后,对于环形队列的判空判满简直不要太简单,明白了环形队列的判空判满对于环形队列剩余的知识当然也不要太简单,总而言之: 环形队列没什么重点,重点就在于理解环形队列为什么叫做环形队列,也就是如何实现对空间的循环使用。 取模运算(a % b)可用于获取到一个0~b-1之间的数,常用于对数组越界和数组下标的控制。
2.正式进入CP问题
明白了上述有关环形队列知识的学习,此时顺理成章正式进入有关信号量实操问题,也就是我们一直说的使用信号量来控制生产消费模型的实现,所以接下来我们就来看看,如何让信号量控制环形队列中的临界资源吧!
同理,实操的前提是对相关知识概念非常清晰,首先我们就来谈谈在环形队列中信号量是如何控制资源以及使用信号量实现生产消费模型的特征,也就是基于信号量的生产消费模型的实现原理。此时我们明白,对于生产者和消费者来说它们关注的资源是不一样的,生产者关注的是环形队列,也就是共享资源中的空间资源,而消费者关心的则是共享资源中的数据资源,也就是当我们把一整块共享资源划分为一份一份的临界资源时,此时生产者和消费者之间关心的资源不同,生产者关心的是该共享资源中可写入数据的空间数量,而消费者关心的是生产者写入数据之后产生的数据数量。所以当我们有了这两个抓手之后,信号量实现生产消费模型的问题我们就差不多搞定了,因为我们已经将该生产消费模型中的信号量给找到了,也就是将共享资源中的空间资源看做是生产者的信号量,而共享资源中的数据资源看做是消费者的信号量,结合之前学习的有关信号量的知识,此时我们就知道,只有当共享资源中还有未存储数据的空间,此时才允许某个生产者线程去申请空间信号量(调度器决定),当获取到了空间信号量之后才能继续向后执行,反之等待。只有当共享资源在生产者生产之后,也就是共享资源中有数据资源时,才允许某个消费者线程去申请对应的数据资源,同理获取之后才允许向后执行。当然具体信号量是如何实现没有对应信号量资源时,让线程发生等待,这个由底层代码决定,这里我们不关心,同理我们只关心信号量的使用规则和相应的功能。
如何实现信号量的P/V操作
明白了上述对信号量实现生产消费模型的简易分析,此时我们就知道,在该生产消费模型中存在两个信号量,一个是生产者的空间信号量,一个是消费者的数据信号量,那么此时问题又来了,我们应该如何对这两个信号量进行控制呢?当然对于信号量的控制本质就是使用我们上篇博客中谈到的sem_post接口和sem_wait接口,也就是伴随信号量产生的P/V操作。首先,肯定是要有两个不同的信号量来分别表示空间信号量和数据信号量,当我们在实现代码时我们就可以定义为_space_sem和_data_sem,然后通过信号量初始化接口sem_init进行初始化,并且因为空间资源先天存在,数据资源后天由生产者生产,所以我们将_space_sem初始化为num(环形队列大小决定),_data_sem初始化为0。然后,当多个生产者线程和消费者线程开始同时访问共享资源时,因为空间信号量存在,数据信号量不存在,所以此时生产者线程可以分配到信号量(调度器决定)且对应空间信号量发生P(_space_sem)操作,直到_space_sem数量为0,生产者线程全部等待,而消费者线程由于数据信号量天生不存在,所以在生产者进行V(_data_sem)操作之前,全部等待,只有当_data_sem数量不为0,此时消费者线程才可能分配到信号量且对应数据信号量发生P(_data_sem)操作,完成消费之后同理进行V(_space_sem)操作,将空间资源释放。从而如此以往,循环往复的实现生产和消费。
注意: 生产消费模型并不代表只有当生产者线程生产完成之后,消费者线程才能消费,从之前学习有关BlockQueue模型的生产消费模型我们也能看出,无论是生产者还是消费者只要其抢到了锁并且符合条件变量,那么它就可以执行,只不过对于BlockQueue来说,我们是使用同一把锁的形式来控制生产者和消费者之间的同步关系,从而实现共享资源只能被生产者或者消费者其中一个线程执行,而此时对于CircleQueue模型来说,我们支持生产者和消费者同时访问共享资源,但不支持它们同时访问该共享资源中的同一份临界资源,从而实现同步关系。那么此时这个同步关系主要体现在哪里呢?信号量会给你答案,我们通过控制两个不同信号量之间的P/V操作来实现这一同步机制,从空间信号量产生数据信号量这一个原理我们可以很好理解,也就是只有当进行了生产者生产,数据信号量才会增加,消费者才能消费这一原理,我们能很好的体会到生产者和消费者之间的同步关系。
3.完整代码实现
3.1 单生产单消费
但注意: 上述代码只有在单生产单消费的场景下才能运行,也就是如果想要在多生产多消费的场景下运行,此时我们就要让多线程之间发生互斥,当然也就是通过互斥锁来实现,那么为什么呢?首先明白,虽然像上述这种类对象,本质所有线程在访问该类中的接口时,都会将该类的对象,也就是上述对象给拷贝一份到自己的栈空间,从而让该类中的接口看做是一个可重入函数接口,但是由于此时我们对类对象c_step和p_step(看成局部变量)存在修改数据的行为,所以此时线程之间就会因为竞态条件的发生,造成数据的不确定性,引发线程安全问题,因此我们就需要使用互斥的方式,来保护该类对象的安全。明白了这个原因之后,此时下述进行加锁之后的代码,就是我们要的多生产多消费代码,如下代码所示:
3.2 多生产多消费
最后明白,无论是单生产单消费,还是多生产多消费,本质并没有说谁的效率更高,并不是单纯的认为多生产多消费的效率就更高,如:想要实现多生产多消费就需要进行加锁操作,对于加锁肯定是会浪费效率的,并且由于多生产多消费,那么多线程之间的上下文切换也会造成一定的效率损失,所以具体使用多生产多消费,还是单生产单消费,是需要通过问题场景来决定的,同理,使用信号量和不使用信号量,也需要具体问题具体分析。总而言之:还是那句话,CP问题最大的好处不在于生产者和消费者之间的串行执行过程,而是在于线程间的解耦和并发问题,也就是之前说过的,线程在等待期间(无论是信号量、互斥锁)它可以访问其它资源(无论是否为共享资源),从而让系统内部的资源利用率提高,从而增强系统的响应速度。