目录
🍊一、什么是生产者消费者模型
🍊二、基于BlockingQueue的生产者消费者模型
🍊三、生产消费模型的upgrade版本
🍊 四、三线程实现生产消费和存储
🍊一、什么是生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯。所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列。消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列是用来给生产者和消费者解耦的。
类比到生活,生产者类似于供应商,消费者类似于学生之类的消费者,而他们中间还有一个缓冲区,就是超市。供应商(生产者)不直接供应商品给消费者,而是供应给超市,消费者从超市买商品。
其中,生产者和生产者之间是互斥关系。消费者和消费者之间是互斥关系。生产者和消费者之间是互斥关系(当生产者和消费者访问同一份资源,在同一时间必须保证只有一个在访问)。而同步关系就是当有货时通知消费者来消费,当货少时通知生产者生产。
总结:"321"原则
3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥(保证共享资源的安全性)&&同步)
2种角色:生产者线程,消费者线程
1个交易场所:一段特定结构的缓冲区
特点:
1、生产线程和消费线程进行解耦
2、支持生产和消费的一段时间的忙闲不均的问题
3、提高效率
🍊二、基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出。
C++ queue模拟阻塞队列的生产消费模型
代码:为了方便理解,这里先以单生产者,单消费者来进行讲解。
这个模型是简单的单生产单消费模型,而且生产消费的是int类型简单数据。后序我们会把他升级,可以生产消费一个类。
通过演示生产消费模型,观察发现,生产数据和消费数据一模一样,这是因为生产数据时我们让它每生产一个数据,sleep(1),而消费数据比较快。当生产完通知消费时,取的都是最新生产的数据,生产一个消费一个。
而如果我们让生产的快,消费的慢,就会发现一瞬间先生产满空间,然后趋于稳定生产一个消费一个,而消费的数据是历史队列上前面的数据,因为队列是FIFO的。
细节1
但是我们要注意一个细节,当生产者被条件变量阻塞时,它是带着锁被阻塞的。而我们的生产者和消费者使用的是同一个锁,按照以前的知识,锁只能被一个线程占有,但是这里生产被阻塞时并没有影响消费者消费数据,这是为什么?
这里就要谈到pthread_cond_wait()的第二个参数必须是我们正在使用的互斥锁。pthread_cond_wait():该函数在调用的时候,会以原子性的方式将锁释放,并将自己挂起。该函数在被唤醒返回的时候会自动地重新获取你传入的锁。这就是我们为什么不担心锁的问题。
细节2
充当条件判断的语法必须是while,不能是if。
原因是我们的代码唤醒是一次唤醒一次生产。如果此时我们的生产数据只有一份空缺,而消费数据时代码中唤醒不是一次生产(pthread_cond_signal),而是全部唤醒(pthread_cond_broadcast),如果是if条件判断,他们会接着之前的代码往下执行,这时多个线程生产数据而空缺只有一份,会出现生产错误!唤醒后需要再次判断(提高代码的健壮性)。
细节3
pthread_cond_signal()这个函数可以放在临界区内部,也可以放在临界区外部。
所以可以先解锁再唤醒。
因为一旦唤醒,生产者或者消费者就会重新拿回锁,而这时消费者或者生产者还没有解锁,需要等待他们解锁,而先解锁再唤醒就不需要等待了。
基于这些,我们再对代码进行修改,生产消费类而不是简单的int。
🍊三、生产消费模型的upgrade版本
BlockQueue.hpp中写了一个阻塞队列,阻塞队列的私有成员变量是一把锁和生产者和消费者的条件变量以及一个队列和队列最大容量。阻塞队列中主要成员函数是push()和pop(),push()是生产者调用的函数,它用于生产任务到队列中,而pop()是消费者调用的函数,用于处理阻塞队列中的任务然后将处理过的任务pop出队列。Task.hpp中是对任务封装的一个类,它的成员是两个操作数一个操作符以及一个回调函数,这个任务类是阻塞队列的基本元素。Main.Cp.cc中是主函数,它的逻辑就是创建两个线程,一个是生产者线程,一个是消费者线程。
🍊 四、三线程实现生产消费和存储
实现不同线程执行不同任务:生产者派发任务,消费处理任务,记录任务结果(将结果记录在文件中)。
p线程生产,并将任务存储到计算队列中
c线程从计算队列中获取任务,然后处理任务,并将处理结果保存到保存队列中
s线程从保存队列中获取处理结果,并将结果保存到文件中。
代码:
上面只是一个生产者,一个消费者,一个存储者。可以改成多生产,多消费的模型。
可以实现多生产多消费。
无论生产还是消费,因为是加锁的,会进入阻塞队列,所以任意时刻只有一个线程生产或者消费,是线程安全的!
🖊生产消费模型高效在哪里?
对生产者而言,向blockqueue里面放置任务,而对消费者而言,是从blockqueue里面拿取任务。
那么生产者的任务从哪里来?生产者获取任务和构建任务要不要花时间?对于消费者,难道它把任务从任务队列中拿出来就结束了吗?消费者拿到任务之后,后续还有没有任务?
🍊都是需要花费时间的!如果生产者是从数据库等中获取数据,十分消耗时间,多线程生产者的好处在于这样当一个线程在获取构建任务时会有其他生产者线程进行生产之前的处理。同理,如果消费者处理任务也很消耗时间,单线程消费者处理任务耗时比较长,多线程的好处就是可以在消费之后,线程并行执行,提高效率。
多个线程并发处理,并不互相影响。生产者和消费者模型并不高效在加锁,它一次还是只能让一个线程处理。而是在生产之前和消费之后,可以让线程并行执行!
类似于虽然生产和消费的过程是加锁的,但是准备工作是可以并行执行的,比如生产饺子是加锁的,但是准备饺子馅,饺子皮(生产之前)和对饺子进行包装(消费处理之后)是可以提前并发准备的!