生产者-消费者问题(转)

发布时间 2023-04-20 17:42:54作者: 大黑耗

原文:https://zhuanlan.zhihu.com/p/259541449

又称有限缓冲问题(英语:Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法[1]等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

生产者-消费者模型解决的问题

在生产者/消费者模型中,生产者Producer负责生产数据,而消费者Consumer负责使用数据。多个生产者线程会在同一时间运行,生产数据,并放到内存中一个共享的区域

生产者线程-消费者线程:

生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;

而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:

  • 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
  • 如果共享数据区为空的话,阻塞消费者继续消费数据;

生产者消费者模型的优点

支持并发

由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,

就可以继续生产下一个数据,而消费者只需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。

接上面的例子,如果我们不使用邮筒,我们就得在邮局等邮递员,直到他回来,

我们把信件交给他,这期间我们啥事儿都不能干(也就是生产者阻塞),或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。

解耦

假设生产者和消费者分别是两个类。

如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。

将来如果消费者的代码发生变化, 可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。

举个例子,我们去邮局投递信件,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。

有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须 得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。

这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。

万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。

而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。

支持忙闲不均

缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。

当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。 等生产者的制造速度慢下来,消费者再慢慢处理掉。

为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,

需要寄出去的信超过1000封,这时 候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来 时再拿走。

 举例:
 1 #生产者和消费者,使用生成器的方式,就是一个简单的并行,
 2 import time
 3 # 这是一个消费者 一直在等待完成吃包子的动作
 4 def consumer(name):
 5     print('%s准备打小三了!'%name)  #打印出对应的消费者的名字
 6     while True:   #执行一个死循环 实际上就是需要调用时才会执行,没有调用就会停止在yield
 7         baozi = yield  #在它就收到内容的时候后就把内容传给baozi
 8         print('小三【%s】来了,被【%s】打了'%(biaozi,name))
 9 def producer(name):
10     c1 = consumer('A')  #它只是把c1变成一个生成器
11     c2 = consumer('B')
12     c1.__next__() #第一个next只是会走到yield然后停止
13     c2.__next__()
14     print('婊子开始做小三了')
15     for i in range(1,10):
16         time.sleep(1)
17         print('三年打了两个婊子')
18         c1.send(i)  #这一步其实就是调用next方法的同时传一个参数i给field接收,然后baozi=i
19         c2.send(i+1)
20         #其实这里是这样的,在send的时候只是继续执行yield下面的语句,然后去去yield,再次停在这儿
21 
22 # producer('aea')
23 c = consumer('aaa') #没next一次就会将程序执行一次
24 c.__next__()
25 c.__next__()
26 c.__next__()
27 **out:**
28 
29 
30 A准备打小三了!
31 B准备打小三了! 
32 婊子开始做小三了
33 三年打了两个婊子
34 小三【1】来了,被【A】打了
35 小三【2】来了,被【B】打了
36 三年打了两个婊子
37 小三【2】来了,被【A】打了
38 小三【3】来了,被【B】打了
39 三年打了两个婊子
40 小三【3】来了,被【A】打了
41 小三【4】来了,被【B】打了
42 三年打了两个婊子
43 小三【4】来了,被【A】打了
44 小三【5】来了,被【B】打了
45 三年打了两个婊子
46 小三【5】来了,被【A】打了
47 小三【6】来了,被【B】打了
48 三年打了两个婊子
49 小三【6】来了,被【A】打了
50 小三【7】来了,被【B】打了
51 三年打了两个婊子
52 小三【7】来了,被【A】打了
53 小三【8】来了,被【B】打了
54 三年打了两个婊子
55 小三【8】来了,被【A】打了
56 小三【9】来了,被【B】打了
57 三年打了两个婊子
58 小三【9】来了,被【A】打了
59 小三【10】来了,被【B】打了
60 光头强准备打小三了!
61 小三【None】来了,被【光头强】打了
62 小三【None】来了,被【光头强】打了