网站如何做的有特色,垂直门户网站的盈利模式探讨,做公司网站需要的资料,wordpress dux主题1.8#x1f493;博主CSDN主页:麻辣韭菜#x1f493; ⏩专栏分类#xff1a;Linux初窥门径⏪ #x1f69a;代码仓库:Linux代码练习#x1f69a; #x1f339;关注我#x1faf5;带你学习更多Linux知识 #x1f51d; 前言 1. 生产消费者模型
1.1 什么是生产消… 博主CSDN主页:麻辣韭菜 ⏩专栏分类Linux初窥门径⏪ 代码仓库:Linux代码练习 关注我带你学习更多Linux知识 前言 1. 生产消费者模型
1.1 什么是生产消费者模型
1.2 生产消费者模型原则
1.3 生产消费者模型的优点 2. 基于阻塞队列实现生产消费者模型
2.1 单生产单消费模型
2.2 多生产多消费
3. POSIX 信号量
POSIX 信号量有两种类型
POSIX 信号量的基本操作
4. 基于循环队列实现生产消费者模型
4.1 多生产多消费
环形队列的优缺点
阻塞队列的优缺点 前言 生产者-消费者模型是一个经典的并发编程问题它描述了两种角色生产者和消费者。生产者负责生成数据而消费者则负责消费这些数据。这个模型通常用于处理多线程或多进程环境中的资源分配问题。 1. 生产消费者模型
1.1 什么是生产消费者模型 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。 生产者和消费者彼此之间不直接通讯而通过阻塞队列来进行通讯所以生产者生产完数据之后不用等待消费者处理直接扔给阻塞队列消费者不找生产者要数据而是直接从阻塞队列里取。 阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。 上面的名词有些抽象我们直接用生活中案例来举例子大家就会豁然开朗。 超市工作模式
超市需要从工厂拿货工厂则需要提供给超市商品
消费者在超市消费超市需要向顾客提供商品 超市的作用就是平衡消费者和工厂供需平衡
为什么这么说
简单来说就是要做到 顾客可以在超市买到想要购买的商品工厂也能同超市完成足量的需求订单超市这样就可以为双方提供便利。
顾客再也不用到工厂去买商品
工厂也不需要将商品亲自送到顾客手中。
如果没有超市顾客直接去工厂消费工厂生产出来商品再送到顾客手中这种关系就是高度相互依赖离开谁都不能干。这就是传说中的强耦合关系。
超市的出现极大了提高效率从而顾客和工厂之间不再单方面的依赖。使得它们之间依赖度降低。而这就是传说的中解耦。
生产者消费者模型的本质忙闲不均
我们再回到编程的视角 工厂 — 生产者 顾客 — 消费者 超市 — 某种容器
这样我们就可以利用线程来干事了线程充当生产者和消费者。利用STL的队列容器缓冲区充当超市。 常见的有 阻塞队列 和 环形队列
在实现中超市不可能只面向一个顾客一个工厂。在多线程中也就意味着它们都能看到这个队列超市那么必须就要让线程之间存在互斥与同步。对于互斥与同步不理解的可以看 Linux 线程的同步与互斥
从上面我们就可以的得出它们之间关系。 生产者VS生产者互斥 一张图解释一切这么多汽车生产商相互竞争对于多线程之间也是一样所以需要互斥。
消费者VS消费者互斥
比如宝马4S店里只剩最后一辆宝马7系如果这时来了两个消费者张三李四都想要这辆车如果是张三先交了订金那么李四就没有机会了但是如果李四私下愿意加钱。那么张三和李四之间存在竞争。对于线程来说我们需要互斥。
生产者VS消费者互斥、同步
我们假设李四拿到了车但是张三是个非常执着的人其他车都不要就要宝马7系。对于4S店来说它就应该给工厂发消息生产7系车。然后再告诉张三有车了进而消费。就对于生产线程和消费线程那就是同步。
如果宝马一直疯狂生产也不管4S店到底卖出去没有也不管消费者到底买不买那么这样就乱套了。结局只有破产所以需要根据消费者的需求来进行合理生产。反过来消费者和宝马也是同理。而这对于多线程来说那就是互斥。
1.2 生产消费者模型原则
生产消费者模型原则321原则 三种关系 生产者VS生产者互斥消费者VS消费者互斥生产者VS消费者同步、互斥 两种角色 生产者消费者 一个交易场所 特定的容器阻塞队列、环形队列 生产消费者模型原则书本是没有这个概念为了方便记忆大牛提炼总结出来的。
1.3 生产消费者模型的优点 为什么生产消费者模型高效
生产者、消费者 可以在同一个交易场所中进行操作生产者在生产时无需关注消费者的状态只需关注交易场所中是否有空闲位置消费者在消费时无需关注生产者的状态只需关注交易场所中是否有就绪数据可以根据不同的策略调整生产者于与消费者间的协同关系 生产消费者模型可以根据供需关系灵活调整策略做到忙闲不均。生产者和消费者无需关心他人的状态做到并发。 2. 基于阻塞队列实现生产消费者模型
在正式编写代码前我们先了解阻塞队列与普通的队列区别在于当队列为空时从队列获取元素的操作将会被阻塞直到队列中被放入了元素当队列满时往队列里存放元素的操作也会被阻塞直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的线程在对阻塞队列进程操作时会被阻塞) 2.1 单生产单消费模型
为了方便理解我们先用单生产、单消费的方式来讲解
先创建Blockqueue.hpp的头文件。
#include iostream
#include queue
#include pthread.htemplate class T
class Blockqueue
{static const int defaultnum 10;public:Blockqueue(int maxcap defaultnum): _maxcap(maxcap){pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_c_cond, nullptr);pthread_cond_init(_p_cond, nullptr);}void push(const T data) //生产数据{}T pop() //取数据{}~Blockqueue(){pthread_mutex_destroy(_mutex);pthread_cond_destroy(_c_cond);pthread_cond_destroy(_p_cond);}private:std::queueT _q;int _maxcap; // 极值pthread_mutex_t _mutex;pthread_cond_t _c_cond; // 消费者pthread_cond_t _p_cond; // 生产者
};
阻塞队列框架搭建出来后生产和消费我们后面实现。
由于我们是单生产单消费的生产消费者模型。所以
在mian.cc主函数中创建两个线程
#include Blockqueue.cppvoid * Consumer(void *args) //消费者
{}
void * Productor(void *args) //生产者
{}int main()
{Blockqueueint *bq new Blockqueueint;//创建线程生产、消费pthread_t c,p;pthread_create(c,nullptr,Consumer,bq);pthread_create(p,nullptr,Productor,bq);pthread_join(c,nullptr);pthread_join(p,nullptr);delete bq;return 0;
}
上面就是生产消费者模型的大致框架我们在实现具体细节之前我们先要明白一个关键问题。
生产和消费要不要耗费时间
生产和消费是肯定要耗费时间的一辆车不会平白无故的出现车从生产到成品这个过程是要耗费大量的数据同理作为消费者使用车也是要耗费时间的。开车不需要耗费时间吗
所以在代码层面角度来说生产和消费都是需要耗费时间的并不是一味的在阻塞队列里进行生产和消费。而是生产者在生产数据之前要对数据做加工做完之后才放进阻塞队列消费者也不是从阻塞队列拿到数据就完事了而是拿到数据之后对数据做分析然后决策。
为什么生产和消费只需要同一把锁
因为它们两个是基于阻塞队列的我们可以把阻塞队列看成一份整体资源所以只需要一把锁但是共享资源也可以被看做多份。
为什么生产和消费各自需要一个条件变量
这就是为什么叫做阻塞队列。两个线程各自基于自己的条件变量当条件不满足时候那么就会阻塞等待。
明白这点之后 我们来实现生产和消费 生产和消费都能看到同一个阻塞队列之前我们也说了生产和消费是既有同步又互斥的关系那么生产线程和消费线程在访问阻塞队列时只能是只有一个在访问。那么必然要互斥 void push(const T data) //生产数据{pthread_mutex_lock(_mutex);_q.push(data);pthread_mutex_unlock(_mutex);} 生产是想生产就能生产的吗
当然不是阻塞队列如同超市一样商品在货架上都放满了生产出来的商品没有人买那不是妥妥亏钱
所以在生产之前还得问问超市条件满足不满足生产不满足堵塞等待被唤醒 void push(const T data) //生产数据{pthread_mutex_lock(_mutex);if(_q.size() _maxcap) {pthread_cond_wait(_p_cond,_mutex);//不满足阻塞}_q.push(data);pthread_cond_signal(_c_cond);pthread_mutex_unlock(_mutex);}
当生产条件不满足的时候那么生产线程要去等待。这里就有个问题生产线程在访问条件满不满足的时候是已经拿到了锁的不释放锁去等待那么会造成死锁的问题。所以我们利用
pthread_cond_wait函数 等待的同时解锁。 同理消费数据也是一样。 T pop() //消费数据{pthread_mutex_lock(_mutex);if(_q.size() 0) {pthread_cond_wait(_c_cond,_mutex);//不满足阻塞}T out _q.front();_q.pop();pthread_cond_signal(_p_cond);pthread_mutex_unlock(_mutex);return out;} 那么我们在实现了生产和消费之后就需要在mian.cc中实现生产消费的回调函数
我们先srand函数模拟随机数
srand(time(nullptr) ^ getpid());
#include ctime
#include unistd.h
void *Consumer(void *args) // 消费者
{Blockqueueint *bq static_castBlockqueueint *(args);while (true){int t bq-pop();std::cout 消费了一个数据... t std::endl;}
}
void *Productor(void *args) // 生产者
{Blockqueueint *bq static_castBlockqueueint *(args);while (true){int data rand() % 10 1;bq-push(data);std::cout 生产了一个数据... data std::endl;sleep(1);}
}结果符合预期生产和消费实现了同步互斥。但是我们就传入个整数未免有点锉了我们是用C写的而且我们blockqueue是带模板我们可以传入对象。
先创建一个Task.hpp的头文件
我们在Task.hpp这个头文件中创建一个Task类。在这个类中实现一些加减乘除的函数方法由生产者生产任务。然后消费者拿到任务数据做加工
#pragma once
#include iostream
#include stringstd::string opers -*/%;enum
{DivZero 1,ModZero,Unknown
};class Task
{
public:Task(int data1, int data2, char oper): _data1(data1), _data2(data2), _oper(oper), _result(0), _exitcode(0){}void run(){switch (_oper){case :_result _data1 _data2;break;case -:_result _data1 - _data2;break;case *:_result _data1 * _data2;break;case /:{if (_data2 0)_exitcode DivZero;else_result _data1 / _data2;}break;case %:{if (_data2 0)_exitcode ModZero;else_result _data1 % _data2;}break;default:_exitcode Unknown;break;}}std::string GetResult(){std::string r std::to_string(_data1);r _oper;r std::to_string(_data2);r ;r std::to_string(_result);r [code: ;r std::to_string(_exitcode);r ];return r;}std::string GetTask(){std::string r std::to_string(_data1);r _oper;r std::to_string(_data2);r ?;return r;}void operator()() //运算符重载让对象像函数一样使用{run();}~Task(){}private:int _data1;int _data2;char _oper;int _result;int _exitcode;
}; void *Consumer(void *args) // 消费者
{// Blockqueueint *bq static_castBlockqueueint *(args);BlockqueueTask *bq static_castBlockqueueTask *(args);while (true){Task t bq-pop();t();std::cout 处理任务: t.GetTask() 运算结果是 t.GetResult() thread id: pthread_self() std::endl;}
}
void *Productor(void *args) // 生产者
{// Blockqueueint *bq static_castBlockqueueint *(args);BlockqueueTask *bq static_castBlockqueueTask *(args);int len opers.size();while (true){int data1 rand() % 10 1;int data2 rand() % 10;char oper opers[rand() % len];Task t(data1, data2, oper);bq-push(t);std::cout 生产了一个任务: t.GetTask() thread id: pthread_self() std::endl;sleep(1);}
} 注 其实我们不用非要等到满了才停止生产。我们可以定策略就如同水库的警戒线当河水上涨到警戒线时就开闸放水而不是等到水库满了才放。消费也是同理。 int low_water_;int high_water_;
2.2 多生产多消费
我们实现了单生产单消费这里改成多生产多消费非常简单。只需要在mian.cc这里循环创建线程即可
int main()
{srand(time(nullptr) ^ getpid());BlockqueueTask *bq new BlockqueueTask;// 创建线程生产、消费pthread_t c[3], p[5];for (int i 0; i 3; i){pthread_create(c i, nullptr, Consumer, bq);}for (int i 0; i 5; i){pthread_create(p i, nullptr, Productor, bq);}for (int i 0; i 3; i){pthread_join(c[i], nullptr);}for (int i 0; i 5; i){pthread_join(p[i], nullptr);}delete bq;return 0;
} 出现上面的错误是因为伪唤醒的原因
为什么会出现伪唤醒的 现在是多个线程了也就是说当阻塞队列满时所有的生产线程被阻塞等待被唤醒。消费线程这时消费一个数据当阻塞队列不满时那么就会唤醒所有的生产线程3个线程只有一个线程能拿到锁其中一个拿到锁线程进行生产此时阻塞队列已经满了。等其他线程拿到锁后条件不满足。生产不了这就是伪唤醒。
所以我们把if改成while 循环判断防止伪唤醒
void push(const T data) // 生产数据{pthread_mutex_lock(_mutex);while (_q.size() _maxcap) // 用while防止伪唤醒,判断条件满不满足{pthread_cond_wait(_p_cond, _mutex); // 不满足阻塞}_q.push(data);pthread_cond_signal(_c_cond);pthread_mutex_unlock(_mutex);}T pop() // 消费数据{pthread_mutex_lock(_mutex);while (_q.size() 0) // 用while防止伪唤醒,判断条件满不满足{pthread_cond_wait(_c_cond, _mutex); // 不满足阻塞}T out _q.front();_q.pop();pthread_cond_signal(_p_cond);pthread_mutex_unlock(_mutex);return out;} 这里我们直接用C的锁。 std:: mutex _mutex;
void *Consumer(void *args) // 消费者
{// Blockqueueint *bq static_castBlockqueueint *(args);BlockqueueTask *bq static_castBlockqueueTask *(args);while (true){Task t bq-pop();t();std::lock_guardstd::mutex guard(_mutex);std::cout 处理任务: t.GetTask() 运算结果是 t.GetResult() thread id: std::hex pthread_self() std::endl;}
}
void *Productor(void *args) // 生产者
{int len opers.size();// Blockqueueint *bq static_castBlockqueueint *(args);BlockqueueTask *bq static_castBlockqueueTask *(args);while (true){ sleep(1);int data1 rand() % 10 1;int data2 rand() % 10;char oper opers[rand() % len];Task t(data1, data2, oper);bq-push(t);std::lock_guardstd::mutex guard(_mutex);std::cout 生产了一个任务: t.GetTask() thread id: std::hex pthread_self() std::endl;}
} 为什么只修改线程创建的代码多线程就能适应原来的消费场景
原因有2点
生产者、消费者都是在对同一个 _queue 操作用一把锁保护一个临界资源足够了当前的 _queue 始终是被当作一个整体使用的无需再增加锁区分
当然也可以让生产者和消费者各自拿一把锁但是都是基于_queue的完全没有必要画蛇添足。
3. POSIX 信号量
在 POSIX 标准中信号量semaphore是一种用于控制多个进程或线程对共享资源访问的同步机制。信号量是一个计数器它可以跟踪一定数量的资源或信号量单位。进程或线程可以通过原子操作对信号量进行增加或减少从而实现对共享资源的协调访问。 也就是说让线程的同步的方法不仅仅只有条件变量还有信号量。 POSIX 信号量有两种类型 无名信号量Unnamed semaphores也称为进程间信号量因为它们可以在不同的进程之间共享。无名信号量使用 sem_t 类型表示并通过 sem_init() 函数初始化使用 sem_destroy() 函数销毁。无名信号量需要一个与之关联的键值来标识这个键值可以通过 ftok() 或 shmget() 函数获得。 命名信号量Named semaphores也称为系统V信号量它们是系统范围内唯一的并且可以跨会话使用。命名信号量通过 semget() 函数创建使用 semctl() 函数控制使用 semop() 函数进行操作。 文档的话太抽象了下面我用大白话来解释信号量 我们将阻塞队列比喻成电影院而信号量就如同电影票电影院是一个整体的公共资源那么电影院的座位就把电影院这个整体划分为无数份的资源。而信号量就是预定座位资源。 那么当我们购买电影票成功或不成功对应编程来说其实就是在访问临界资源的同时进行了临界资源就绪或者不就绪判断。 就绪意味者线程可以访问 不就绪意味着线程不可访问 POSIX 信号量的基本操作
初始化使用 sem_init() 初始化一个无名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem指向信号量变量的指针。pshared非零表示信号量可以被其他进程访问零表示只能在当前进程内访问。value信号量的初始值。 等待减使用 sem_wait() 或 sem_trywait() 减少信号量如果信号量的值大于零则减少其值否则进程将等待。
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
信号量值增加信号使用 sem_post() 增加信号量的值如果其他进程因为信号量的值小于或等于零而等待则其中一个进程将被唤醒。
int sem_post(sem_t *sem);
获取信号量值使用 sem_getvalue() 获取信号量的当前值。
int sem_getvalue(sem_t *sem, int *sval);
销毁信号量使用 sem_destroy() 销毁一个无名信号量。
int sem_destroy(sem_t *sem); 这些接口使用起来还是比较简单下面我们用信号量来实现生产消费者模型。前面用的是阻塞队列我们用信号量实现基于循环队列版本。
4. 基于循环队列实现生产消费者模型
在实现之前我们先了解循环队列这种数据结构。我们利用数组这种数据结构然后对下标进行取模可以让数组变成循环的结构 一张动图搞定循环队列这种数据结构
这里有几个关键问题
问题1生产者关注什么资源消费者关注什么资源 生产者关注的是数组还有多少空间、消费者关注的是数组还有多少数据。 问题2生产者和消费者什么时候才会指向同一个位置 要么数组为空、要么数组为满。这两种状态只能是生产和消费其中一个进行访问空生产者访问、满消费者访问。 反之一定是指向不同的位置 这句话非常重要意味着生产和消费可以同时访问 那么循环队列要正常运行必须满足3个条件
1. 空或者满只能有一个人访问
2. 消费者一定不能超过生产者
3. 生产者一定不能套圈消费者 如果消费者超过生产者前面都没有数据访问什么 为什么这么说因为最开始一定为空。那么一定是生产者先走毫无疑问 如果生产者套圈消费者意味着生产速度大于消费速度之前没有消费的数据要被覆盖。数据出现覆盖严重错误。 理解了这些问题我们直接多生产多消费来实现
4.1 多生产多消费
老规矩先创建RingQueue.hpp头文件
#include iostream
#include vector
#include pthread.h
#include semaphore.h
const static int defaultcap 5;
template class T
class RingQueue
{
public:RingQueue(int cap defaultcap): _ringqueue(cap), _cap(cap), _c_step(0), _p_step(0)sem_init(_cdata_sem, 0, 0);sem_init(_pspace_sem, 0, cap);pthread_mutex_init(_c_mutex, nullptr);pthread_mutex_init(_p_mutex, nullptr);}void push(const T data){}T pop(T* out){}~RingQueue(){sem_destroy(_cdata_sem);sem_destroy(_pspace_sem);pthread_mutex_destroy(_c_mutex);pthread_mutex_destroy(_p_mutex);}private:std::vectorT _ringqueue; // 循环队列int _cap; // 循环队列容量int _c_step; // 消费者下标int _p_step; // 生产者下标sem_t _cdata_sem; // 消费者关注的数据资源sem_t _pspace_sem; // 生产者关注的空间资源pthread_mutex_t _c_mutex; // 消费者锁pthread_mutex_t _p_mutex; // 生产者锁
};
框架大致构建出来为了方便生产消费的互斥与同步。我们接下来对生产和消费线程互斥与同步的函数进行封装
void Lock(pthread_mutex_t mutex){pthread_mutex_lock(mutex);}void UnLock(pthread_mutex_t mutex){pthread_mutex_unlock(mutex);}void P(sem_t sem) //减少{sem_wait(sem);}void v(sem_t sem) //增加{sem_post(sem);}实现push 和 pop函数 void Push(const T data){P(_pspace_sem);Lock(_p_mutex);_ringqueue[_p_step] data;_p_step % _cap;UnLock(_p_mutex);V(_cdata_sem);}T Pop(T *out){P(_cdata_sem);Lock(_c_mutex);*out _ringqueue[_c_step];_c_step % _cap;Unlock(_c_mutex);V(_pspace_sem);return out;} 这里解释push函数P操作为什么传入的是空间信号量很简单生产者关注的是空间资源所以这里P判断空间资源就不就绪V为什么传入的是数据信号量当P申请成功意味着可以生产那么对应空间资源减少数据资源增加。
同理pop也是一样。
我们mian.cc创建线程 和回调函数
#include unistd.h
#include mutex
#include ctime
#include RingQueue.hpp
#include Task.hpp
std::mutex _mutex;
void *consumer(void *args)
{RingQueueTask *rq static_castRingQueueTask *(args);while (true){Task t;rq-Pop(t);t();std::lock_guardstd::mutex guard(_mutex);std::cout 处理任务: t.GetTask() 运算结果是 t.GetResult() thread id: std::hex pthread_self() std::endl;}
}
void *productor(void *args)
{RingQueueTask *rq static_castRingQueueTask *(args);int len opers.size();while (true){sleep(1);int data1 rand() % 10 1;int data2 rand() % 10;char oper opers[rand() % len];Task t(data1, data2, oper);rq-Push(t);std::lock_guardstd::mutex guard(_mutex);std::cout 生产了一个任务: t.GetTask() thread id: std::hex pthread_self() std::endl;}
}int main()
{srand(time(nullptr) ^ getpid()); // 随机数种子RingQueueTask *rq new RingQueueTask(40);pthread_t c[3], p[3];for (int i 0; i 3; i){pthread_create(c i, nullptr, consumer, rq);}for (int i 0; i 3; i){pthread_create(p i, nullptr, productor, rq);}for (int i 0; i 3; i){pthread_join(c[i], nullptr);}for (int i 0; i 3; i){pthread_join(p[i], nullptr);}delete rq;return 0;
} 这里打印只打印了线程ID我们可以重新创建一个线程名字的类。把线程名字加入进去
struct ThreadData
{RingQueueTask *rq;std::string threadname;
}; 细节 加锁行为放在信号量申请成功之后可以提高并发度 为什么这么说信号量在加锁之前就好比没进电影院之前就已经选好了座位如果在加锁之后那就如同进到电影院之后在选座位而再选座位就又得排队买票。而且信号量本身就是原子操作。
那既然阻塞队列也能实现生产消费者模型那搞出来个循坏队列又有什么用
环形队列的优缺点
优点
空间利用率高由于是环形结构已使用的空间可以重复利用不会像普通队列一样造成空间的浪费。插入和删除速度快由于是线性结构环形队列的插入和删除操作通常很快因为它们只涉及到头尾指针的移动。固定大小的存储空间可以避免内存泄漏等问题因为不会动态地分配和回收内存。
缺点
需要额外的指针维护状态增加了复杂度需要维护队列头和队尾的指针。存储空间可能未被充分利用一旦队列满了就需要覆盖队列头的元素这可能导致存储空间没有被完全利用。队列大小必须预先定义难以动态调整大小这在某些需要灵活内存使用的场景下可能是一个限制。 阻塞队列的优缺点
优点
线程同步阻塞队列可以很好地实现线程之间的同步简化了生产者和消费者之间的数据传递和通信。解耦合作为生产者消费者模式的缓冲空间阻塞队列降低了生产者和消费者之间的耦合性。削峰填谷由于阻塞队列的大小是有限的它可以起到限制作用平衡突发的流量高峰。
缺点
可能引发死锁如果使用不当比如生产者和消费者互相等待对方释放资源时可能会发生死锁。对性能的影响线程的挂起和唤醒操作可能会对系统性能产生影响尤其是在高并发场景下。处理超时操作较复杂在设置了超时时间的情况下需要处理超时异常并进行相应的补偿或回滚操作增加了编程复杂性。
每种数据结构都有其特定的使用场景和限制开发者在选择时应根据具体需求和上下文来决定使用哪一种。 本篇我们学习了什么是生产消费者模型基于两种数据结构分别实现了生产消费者模型 还掌握了一个线程同步神奇——信号量。这对于提高线程之间的并发度非常有用。再次理解了生产消费者模型为什么高效总之生产消费者模型非常值得我们学习。