Linux线程——基于生产者消费者模型的线程同步互斥应用
文章目录
- 基于生产者消费者模型的线程同步互斥应用
- 生产者消费者模型
- 举例解释模型
- 实际应用——对比到多线程情况
- 基于阻塞队列BlockQueue的应用
- 原生线程库的版本
- 使用封装的组件
- 基于环形队列CycleQueue的使用
- 样例区别
基于生产者消费者模型的线程同步互斥应用
上一篇文章,我们学习了Linux线程下,针对于并发执行流可能会出现的问题的解决方案——即线程同步和互斥。本篇文章,将以上一篇文章[Linux线程——线程互斥、同步、POSIX信号量]中讲解内容为基础来进行应用!
生产者消费者模型
本篇文章应用的场景将以生产者——消费者模型为基础。所以,有必要先了解一下该模式。
我们先输出结论,再通过一个生活中的例子来进行讲解:
生产者消费者模型,本质上是进程间通信的一种实现方式!
举例解释模型
现在假设有这么一个场景:
1.有若干个生产产商 -> 生产泡面
2.有若干个客户 -> 购买泡面
3.有一个超市,从厂商购买物资,上架商品,卖给客户
这里就不考虑网购的情况,就以过往的生产/消费关系来说:
泡面生产厂商 <-> 生产者
客户 <-> 消费者
超市 <-> 交易场所
现在有一些问题:
1.为什么消费者不直接去生产者那里直接进货?即生产者为什么不根据消费者需求来进行生产?
这样效率太低下!而且对于生产厂商而言,是绝对划不来的!按照消费者需求进行生产安排,是不足以覆盖生产成本的。
就像我们在使用STL容器的时候,底层的扩容机制是从内存池中获取内存,内存池不久相当于交易场所吗?如果直接向系统申请内存,频繁的申请会导致程序效率降低!
2.生产者如果有多个,消费者也有多个,是否会出现并发问题?
我们今天,可以把生产者和消费者都当作一个个的线程,那么交易场所就是一个公共资源!所以,必然是会存在并发问题的!
比如,交易场所进货的时候,正在登记进货情况!如果有消费者在登记时候直接把产品拿走了,这就会导致进货情况登记错误!对于超市而言,到时候进货出货的帐是对不上的!对账对不上这个问题有多严重想必不用多说!-> 生产消费同步逻辑问题
又或是有多个厂商提供货源时,超市需要根据当前超市内部的空货架情况来安排决定进哪一些货。如果一次性涌入多个厂商来进货,那么万一进货的时候把产品放在一块了,这不便于超市的售卖! -> 线程的互斥问题
3.生产者和消费者是需要根据交易场所的情况来做动作的?
没错,生产者关注的是超市中的货架剩余数量。而消费者关心的是超市中的商品!
1.如果交易场所还有空的 -> 生产厂商就可以生产完后然后上架
2.如果交易场所商品售罄 -> 消费者就没有办法购买商品
实际应用——对比到多线程情况
如上图所示:
其实所谓的生产者和消费者,我们其实都可以通通看成是一个线程!而交易场所,可以看作是一个内存空间(具体以什么形式给线程使用不重要)。交易场所,本身就是一个共享资源!而因为线程间本就是共享同一个进程地址空间的,所以天然的就有通信的条件!
所以,这个模型本质上,就是为了高效地进行线程间通信而设计的一种模式!
但是,因为线程共享资源的时候,会引发一些并发问题:
1.生产者之间需要互斥,消费者之间也需要!(如有多线程情况)
2.针对于生产者和消费者之间,需要通过交易场所的性质来判断二者之间的关系!
3.交易场所是存放通信数据的地方!组织形式可以有多种!
所以,生产者消费者模型有三大基本要素——消费者、生产者、交易场所
为了方便记忆,这里提出321原则!
3 -> 3种关系
消费者之间是互斥关系; 生产者之间也是互斥关系!生产者和消费者之间是同步和互斥!
2 -> 2种角色
即由线程来充当生产者和消费者的角色
1 -> 1个交易场所
可自行选择数据结构来充当交易场所
其中,我们针对于交易场所可以做一个分类:
1.交易场所不能让生产者消费者同时访问
2.交易场所大部分时间可以让消费者生产者同时访问,只在特殊情况下不能
(第二点的原因是交易场所空间是有上限的!)
我们接下来就根据这两点分类,来实现不同场景应用!
基于阻塞队列BlockQueue的应用
首先,我们来针对于第一种情况,生产者消费者是不能同时访问交易场所的!
我们采用一种叫做阻塞队列的结构来进行应用!
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
原生线程库的版本
刚开始用的时候,我们还是需要先熟悉一下线程库的使用,所以第一个版本,我们就使用原生的线程库来进行使用:
BlockQueue.hpp:
#include <iostream>
#include <pthread.h>
#include <queue>// 这个版本的生产者消费者模型的BlockQueue是直接使用pthread原生线程库来进行实现的// 直接使用非类型模板参数定义队列大小 为了方便测试,就提供5个
template<class T, size_t N = 5>
class BlockQueue{
private:bool IsFull(){return _block_queue.size() >= _capacity;
}bool IsEmpty(){return _block_queue.empty();
}// 还有一些细节:
//1. 在条件变量下等待是需要锁的,而且应该在临界区内进行等待!
//2. 首先,等待的时候,会自动释放锁;如果唤醒,就需要重新申请锁,只有申请到锁才能往下执行!
//3. 如果唤醒后一段时间内申请不到锁也是不怕的,因为会在申请锁那里进行等待!
//4. 上述逻辑在cond的相关接口内维护!// 在指定条件变量下等待,应当在临界区内!
// 因为条件变量需要判定临界资源的情况! 如果不在临界区判断,也会导致数据不一致!public:BlockQueue():_capacity(N),_consumer_waitNum(0),_productor_waitNum(0){pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_full, nullptr);pthread_cond_init(&_empty, nullptr);}// queue会自己调用析构~BlockQueue(){pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_full);pthread_cond_destroy(&_empty);}void Push(T data){// 是生产者将生产的东西入队pthread_mutex_lock(&_lock);while(IsFull()){// 如果队列此时处于满的状态,生产者就不能再生产东西入队!// 所以就要在满的条件变量下进行等待++_productor_waitNum;// 但是如果使用if(IsFull())会出问题:// 1. 如果pthread_cond_wait如果说等待失败,也就是这个函数执行不成功,返回非0值//那么此时就不会再执行该函数了,就往下执行。 但是,此时在临界区内(加了锁),只有当前线程能进来访问//此时队列仍然为满!如果使用if判断,那么就直接往下执行了。那么队列为满,还要再往里面push,这就错了// 2. 还有一种情况就是://如果当前消费者就消费了一个,但是消费者那边使用pthread_cond_broadcast把全部生产者唤醒//很可能会出现,连着几个生产者持有锁//但是,消费者那边只消费了一个,但一旦出现上述情况//就会导致,后序的消费者无法消费,但是生产者拿到锁就往下执行了//此时队列仍然是满的!后序线程往里面push,也是错的!// 所以,这就不能使用if来进行判断,而必须使用while(IsFull())来进行判断!pthread_cond_wait(&_full, &_lock);--_productor_waitNum; }// 此时说明可以生产东西放入队列_block_queue.push(data);// 而且此时,一定是有产品给消费者进行消费的 -> 唤醒消费者 -> 前提是,有消费者正在处于等待if(_consumer_waitNum > 0){pthread_cond_signal(&_empty);std::cout << "productor : 唤醒消费者" << std::endl;}// 针对于单消费 单生产的情况// pthread_cond_signal(&_empty);pthread_mutex_unlock(&_lock);// pthread_cond_signal(&_empty);// 单消费单生产的话 -> 每生产一个就通知一次!// 这里在解锁前和解锁后唤醒都无所谓! 因为如果唤醒后:// 在pthread_cond_wait下,是需要传入一把锁// 即如果使用pthread_cond_wait,会自动释放这个锁给别人用。唤醒后,需要重新申请锁,才会继续往下走// 如果唤醒后一段时间内没申请到锁,无所谓,会在申请锁那里阻塞等待!// 所以,这就导致写在解锁前后都无所谓!!!!!}T Pop(){// 这个是消费者拿东西出队消费pthread_mutex_lock(&_lock);// 这里用while和Push接口类似!while(IsEmpty()){// 如果队列此时处于空的状态,消费就不能从队中拿东西消费!// 所以就要在空的条件变量下进行等待++_consumer_waitNum;pthread_cond_wait(&_empty, &_lock);--_consumer_waitNum;}// 此时说明,队列不为空,可以消费T product = _block_queue.front();_block_queue.pop();// 此时就可以进行唤醒生产者 -> 前提是 -> 当前有生产者在等待if(_productor_waitNum > 0){pthread_cond_signal(&_full);std::cout << "consumer : 唤醒生产者" << std::endl;}pthread_mutex_unlock(&_lock);return product;}// 这个接口建议不要使用,只是为了方便在单消费,单生产的情况下输出日志用的T Front(){return _block_queue.front();}private:std::queue<T> _block_queue;size_t _capacity; // 当前阻塞队列的容量pthread_mutex_t _lock; // 交易场所需要的锁pthread_cond_t _full; // 判断是否慢了的条件变量,生产者在队列满的情况下不能pushpthread_cond_t _empty; // 判断是否为空的条件变量,消费者在队列空的情况下不能popsize_t _consumer_waitNum; // 消费者在等待的个数size_t _productor_waitNum; // 生产者在等待的个数
};
Main.cpp:
#include "BlockQueue.hpp"
#include <unistd.h>// 1.单消费 单生产
// 2.多消费 多生产// 2.
#include <functional>using func_t = std::function<void()>;void DownLoad(){std::cout << "this is download" << std::endl;
}template<class T>
void* Product(void* args){BlockQueue<T>* pbq = static_cast<BlockQueue<T>*>(args);while(1){func_t task = DownLoad;std::cout << "productor : 发送了一个任务" << std::endl;pbq->Push(task);// 为了看清楚效果不得以这么做sleep(1);}return nullptr;
}template<class T>
void* Consume(void* args){BlockQueue<T>* pbq = static_cast<BlockQueue<T>*>(args);while(1){T task = pbq->Pop();std::cout << "consumer : 接收一个任务" << std::endl;// 2.执行任务task();// 为了看清楚效果不得以这么做sleep(20);}return nullptr;
}int main(){BlockQueue<func_t>* pbq = new BlockQueue<func_t>;pthread_t consumer1, consumer2, consumer3;pthread_t productor1, productor2;pthread_create(&productor1, nullptr, Product<func_t>, pbq);pthread_create(&productor2, nullptr, Product<func_t>, pbq);pthread_create(&consumer1, nullptr, Consume<func_t>, pbq);pthread_create(&consumer2, nullptr, Consume<func_t>, pbq);pthread_create(&consumer3, nullptr, Consume<func_t>, pbq);pthread_join(consumer1, nullptr);pthread_join(consumer2, nullptr);pthread_join(consumer3, nullptr);pthread_join(productor1, nullptr);pthread_join(productor2, nullptr);return 0;
} // 1./* template<class T>
void* Product(void* args){BlockQueue<T>* pbq = static_cast<BlockQueue<T>*>(args);int data = 1;while(1){++data;std::cout << "productor : 发送了一个数据" << data << std::endl;pbq->Push(data);// 为了看清楚效果不得以这么做sleep(1);}return nullptr;
}template<class T>
void* Consume(void* args){BlockQueue<T>* pbq = static_cast<BlockQueue<T>*>(args);while(1){//T data = pbq->Front();T data = pbq->Pop();std::cout << "consumer : 接收一个数据" << data << std::endl;//pbq->Pop();// 为了看清楚效果不得以这么做sleep(5);}return nullptr;
}int main(){BlockQueue<int>* pbq = new BlockQueue<int>;pthread_t consumer, productor;pthread_create(&productor, nullptr, Product<int>, pbq);pthread_create(&consumer, nullptr, Consume<int>, pbq);pthread_join(consumer, nullptr);pthread_join(productor, nullptr);return 0;
} */
这里需要说的是:
由于现在我们没有对显示器文件进行保护,所以打印出来的效果是比较混乱的!但是,后续我们将会自行实现一个日志系统!到时候就不会出现这样的问题!
使用封装的组件
BlockQueue:
#include <iostream>
#include <pthread.h>
#include <queue>// 这一个版本就是基于前面实现过的封装来进行操作(除了线程创建操作等...)
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace myCond;
using namespace myMutex;// 这个版本的生产者消费者模型的BlockQueue是直接使用pthread原生线程库来进行实现的// 直接使用非类型模板参数定义队列大小 为了方便测试,就提供5个
template<class T, size_t N = 5>
class BlockQueue{
private:bool IsFull(){return _block_queue.size() >= _capacity;
}bool IsEmpty(){return _block_queue.empty();
}// 还有一些细节:
//1. 在条件变量下等待是需要锁的,而且应该在临界区内进行等待!
//2. 首先,等待的时候,会自动释放锁;如果唤醒,就需要重新申请锁,只有申请到锁才能往下执行!
//3. 如果唤醒后一段时间内申请不到锁也是不怕的,因为会在申请锁那里进行等待!
//4. 上述逻辑在cond的相关接口内维护!// 在指定条件变量下等待,应当在临界区内!
// 因为条件变量需要判定临界资源的情况! 如果不在临界区判断,也会导致数据不一致!public:BlockQueue():_capacity(N),_consumer_waitNum(0),_productor_waitNum(0),_full(_lock),_empty(_lock){}// 析构就什么也都不用写了~BlockQueue(){}//使用RAII的思想来进行加锁解锁void Push(T data){// 是生产者将生产的东西入队Lock_Guard guard(_lock);while(IsFull()){// 如果队列此时处于满的状态,生产者就不能再生产东西入队!// 所以就要在满的条件变量下进行等待++_productor_waitNum;// 但是如果使用if(IsFull())会出问题:// 1. 如果pthread_cond_wait如果说等待失败,也就是这个函数执行不成功,返回非0值//那么此时就不会再执行该函数了,就往下执行。 但是,此时在临界区内(加了锁),只有当前线程能进来访问//此时队列仍然为满!如果使用if判断,那么就直接往下执行了。那么队列为满,还要再往里面push,这就错了// 2. 还有一种情况就是://如果当前消费者就消费了一个,但是消费者那边使用pthread_cond_broadcast把全部生产者唤醒//很可能会出现,连着几个生产者持有锁//但是,消费者那边只消费了一个,但一旦出现上述情况//就会导致,后序的消费者无法消费,但是生产者拿到锁就往下执行了//此时队列仍然是满的!后序线程往里面push,也是错的!// 所以,这就不能使用if来进行判断,而必须使用while(IsFull())来进行判断!_full.Wait();--_productor_waitNum; }// 此时说明可以生产东西放入队列_block_queue.push(data);// 而且此时,一定是有产品给消费者进行消费的 -> 唤醒消费者 -> 前提是,有消费者正在处于等待if(_consumer_waitNum > 0){_empty.Signal();std::cout << "productor : 唤醒消费者" << std::endl;}// 单消费单生产的话 -> 每生产一个就通知一次!// 这里在解锁前和解锁后唤醒都无所谓! 因为如果唤醒后:// 在pthread_cond_wait下,是需要传入一把锁// 即如果使用pthread_cond_wait,会自动释放这个锁给别人用。唤醒后,需要重新申请锁,才会继续往下走// 如果唤醒后一段时间内没申请到锁,无所谓,会在申请锁那里阻塞等待!// 所以,这就导致写在解锁前后都无所谓!!!!!}T Pop(){// 这个是消费者拿东西出队消费Lock_Guard guard(_lock);// 这里用while和Push接口类似!while(IsEmpty()){// 如果队列此时处于空的状态,消费就不能从队中拿东西消费!// 所以就要在空的条件变量下进行等待++_consumer_waitNum;_empty.Wait();--_consumer_waitNum;}// 此时说明,队列不为空,可以消费T product = _block_queue.front();_block_queue.pop();// 此时就可以进行唤醒生产者 -> 前提是 -> 当前有生产者在等待if(_productor_waitNum > 0){_full.Signal();std::cout << "consumer : 唤醒生产者" << std::endl;}return product;}// 这个接口建议不要使用,只是为了方便在单消费,单生产的情况下输出日志用的T Front(){return _block_queue.front();}private:std::queue<T> _block_queue;size_t _capacity; // 当前阻塞队列的容量Mutex _lock; // 因为这里封装的Cond是要传入锁的,所以需要保证Mutex在Cond之前定义Cond _full; // 判断是否慢了的条件变量,生产者在队列满的情况下不能pushCond _empty; // 判断是否为空的条件变量,消费者在队列空的情况下不能popsize_t _consumer_waitNum; // 消费者在等待的个数size_t _productor_waitNum; // 生产者在等待的个数
};
Mutex.hpp
#pragma once#include <iostream>
#include <pthread.h>
#include <unistd.h>namespace myMutex{class Mutex{public:Mutex(){int id = pthread_mutex_init(&_lock, nullptr);if(id != 0) {std::cout << "Mutex Init Error!" << std::endl;}}~Mutex(){int id = pthread_mutex_destroy(&_lock);if(id != 0) {std::cout << "Mutex Destroy Error!" << std::endl;}}void Lock(){int id = pthread_mutex_lock(&_lock);if(id != 0) {std::cout << "Mutex Lock Error!" << std::endl;}}void UnLock(){int id = pthread_mutex_unlock(&_lock);if(id != 0) {std::cout << "Mutex UnLock Error!" << std::endl;}}pthread_mutex_t* GetLockAddr(){return &_lock;}private:pthread_mutex_t _lock;};// RAII思想加锁 / 解锁 ,锁的创建和删除由Mutex类自己完成class Lock_Guard{public:Lock_Guard(myMutex::Mutex& lock):_lock(lock){// 创建Lock_Guard的时候自动加锁_lock.Lock();}~Lock_Guard(){_lock.UnLock();}private:Mutex& _lock;};
}
Cond.hpp:
#pragma once
// 这里封装一下条件变量 -> 方便后续使用#include "Mutex.hpp"using namespace myMutex;namespace myCond{class Cond{public:Cond(Mutex& mutex):_lock(mutex){pthread_cond_init(&_cond, nullptr);}~Cond(){pthread_cond_destroy(&_cond); }void Wait(){int wait_id = pthread_cond_wait(&_cond, _lock.GetLockAddr());if(wait_id != 0) {std::cout << "condition wait error !" << std::endl;}}void Signal(){int signal_id = pthread_cond_signal(&_cond);if(signal_id != 0) {std::cout << "condition siganl error !" << std::endl;}}void Broadcast(){int broadcast_id = pthread_cond_broadcast(&_cond);if(broadcast_id) {std::cout << "condition broadcast error !" << std::endl;}}Mutex& GetMutex(){return _lock;}private:pthread_cond_t _cond;Mutex& _lock;};
}
基于环形队列CycleQueue的使用
这里我们就直接看使用封装组件的代码了:
CycleQueue.hpp
#pragma once#include <iostream>
#include <pthread.h>
#include <vector>
#include "Mutex.hpp"
#include "Sem.hpp"// 这里是使用循环队列 + POSIX信号量 来进行生产者消费者模式 -> 直接写多生产,多消费的模式(其实就是多了两把锁)using namespace myMutex;
using namespace mySem;// 信号量的值(生产端先开始) 这里定义为5,方便测试template<class T, size_t N = 5>
class Queue_cycle{
public:Queue_cycle():_capacity(N),_productor_sem(N),_productor_pos(0),_consumer_sem(0),_consumer_pos(0),_queue_cycle(_capacity, T()){}~Queue_cycle(){}void Push(const T& data){// 生产者在这里进行生产// 1.首先需要申请信号量,即生产方预定资源 只有申请到了生产的信号量(能放)才能继续生产入队_productor_sem.P();{// 多线程的情况下要加锁 这个Lock_Guard生命周期在代码块内部!一旦出去就自动解锁了Lock_Guard guard(_productor_lock);//2.入队_queue_cycle[_productor_pos] = data;//3.调整生产位置++_productor_pos;//4.但是,由于是环形队列,所以需要维持环形队列的特性 -> 防止越界_productor_pos %= _capacity;}//5.这个时候,生产的信号量-1,但是拿数据的信号量就可以加1_consumer_sem.V();}T Pop(){// 消费者进行消费// 1.首先需要申请信号量,即消费方预定消费资源 只有申请到了要消费的信号量(能拿)才能从队中取数据_consumer_sem.P();T data;{Lock_Guard guard(_consumer_lock);//2. 出队data = _queue_cycle[_consumer_pos];//3. 调整消费位置++_consumer_pos;//4.但是,由于是环形队列,所以需要维持环形队列的特性 -> 防止越界_consumer_pos %= _capacity;}//5.这个时候,消费的信号量-1,但是放数据的信号量就可以加1_productor_sem.V();return data;}private:Sem _productor_sem;int _productor_pos;Sem _consumer_sem;int _consumer_pos;size_t _capacity;std::vector<T> _queue_cycle;Mutex _productor_lock;Mutex _consumer_lock;
};// 这里稍微解释一下:
// 首先,环形队列的消费者生产者模式需要满足几个基本条件
// 1.队列为满 / 空,就会导致当前的两个下标相同 int _productor_pos; int _consumer_pos;
// 2.但是,当两个下标在一起的时候,需要做一些判断:// 队列满了,生产方不能再生产,只能让消费方先消费// 队列空了,消费方无法消费,只能先让生产方先生产
// 3.在一个循环队列下,生产方不能给消费方完整的套一圈 -> 防止未消费的数据被覆盖
// 4.在一个循环队列下,消费方不能超过生产方,因为没有生产时无法消费// 细节:
// 循环队列下,生产方和消费方只有在队列满了/空了的时候,两个下标重叠,此时生产方和消费方互斥!
// 其余时候,两个下标不可能重叠,所以消费方和生产方可以并发处理!(这是生产方和消费方的关系)// 上面说的所有条件,都可以使用信号量来控制! (以队列大小为N)
// 队列满了:生产方信号量N, 消费方为0 -> 消费方阻塞在申请信号量处
// 队列空了:消费方信号量0, 生产方为N -> 生产方阻塞在申请信号量处
// 在信号量的控制下,是不可能有”套圈“的行为出现的!
// 而且,在信号量的控制下,生产方和消费方的线程的在队列的操作下标是不一样的!互不影响!所以可以并发!// 多生产——多消费的情况:
// 在单——单情况下,不需要维护多个consumer之间的关系,不需要维护多个productor之间的关系 cp之间关系通过信号量维护
// 但在多——多情况下,需要维护c之间,p之间关系!循环队列也是共享资源,所以需要加锁!// 加锁的位置在信号量的申请前后都可以!(使用RAII思想加锁解锁)// 在申请信号量前加锁,就相当于现在多个线程间选取一个出来,再来申请信号量! -> 先排队买票,然后再排队进入场所// 在申请信号量后加锁,就相当于 -> 现在网上买票,再来排队进入场所// 那么肯定是第二种方式效率更高! 而且,没有预定资源(申请信号量本质),就没有必要排队!
Sem.hpp:
#pragma once#include <iostream>
#include <semaphore.h>namespace mySem{class Sem{public:Sem(int default_value = 1, int default_shared_falg = 0){// 第二个参数是信号量在线程间共享还是进程间共享,0是进程间共享// 第三个参数是信号量的值sem_init(&_sem, default_shared_falg, default_value);}~Sem(){sem_destroy(&_sem); }// 对信号量进行原子性的--操作void P(){int n = sem_wait(&_sem);if(n != 0) {std::cout << "sem wait error" << std::endl;}}// 对信号量进行原子性的++操作void V(){int n = sem_post(&_sem);if(n != 0) {std::cout << "sem post error" << std::endl;}}private:sem_t _sem;};
}
还是一样,很多细节和注意事项已经在代码中的注释写过了!
样例区别
阻塞队列中,我们使用的是Mutex + Cond来进行控制;
循环队列中,我们使用的是Mutex + Sem来进行控制;
其中,Mutex锁是必须用的。因为要维护消费者之间的关系,生产者之间的关系!
但是,交易场所一个是循环队列,一个是阻塞队列。但是这两个是有区别的:
阻塞队列我们直接用了STL容器来操作。这个STL容器需要进行增删查改,是动态的,一次只能有生产/消费一方来进行操作。
但是,循环队列不需要!因为循环队列本质上是一个定容数组。生产方和消费方拿着各自的生产位置下标进行操作!所以,大部分时间是不需要处理它们间的关系!只有容量为空/满才需要处理二者之间的关系。
所以,基于上面的两种原因:
前者在维护生产消费关系的时候是需要用锁的。但是后者只需要通过信号量来进行控制!而且使用信号量,天然的就带有判断交易场所容量的逻辑了!就不需要像前者一样需要判断逻辑!
所以,信号量的另一个本质就是:
把对临界资源的判断(就绪、容量、存在等条件),以原子性的形式呈现在访问临界资源之前!
所以:
BlockQueue和CycleQueue最大的区别是:
后者是可以拆分为多份使用(使用Sem),前者只能整体使用(使用Mutex)。