生产者-消费者模型是多线程或多进程中的一个典型的案例,在《操作系统》这门课程中有对其较深的讲解。
概述
生产者消费者模式是一种经典的同步机制,用于解决多线程间的协作问题。这种模式主要涉及两类线程:生产者(Producer)和消费者(Consumer)。生产者负责生成数据,放入一个共享的数据缓冲区中,而消费者则从缓冲区中取出数据进行处理。该模式可以通过Queue模块在Python中实现,其中队列(Queue)即作为那个共享的数据缓冲区。
为什么使用生产者消费者模式
使用生产者消费者模式的主要原因之一是为了解耦生产数据的过程与消费数据的过程。这意味着生产者无需等待消费者处理数据即可继续产生新的数据,反之亦然。这种方式可以大幅提高程序的并发性能和效率,特别是在生产和消费速度不一致时。
在这里我们只需要对该模型有最原始的理解,然后用python实现这个过程。虽然基于python的实现看起来简单,但是在深入了解后我们才能发现里面每一处都有自己的巧妙设计。
写这个博客的目的是重新梳理一下刚学的知识,加深自己的印象!
假设生产者和消费者都是线程(进程的实现类似),写出代码。下面是一个利用Queue模块实现基本的生产者-消费者模型的示例:
import time
import threading
from queue import Queue
def producer(queue):
for i in range(1, 6):
item = f'项{i}'
queue.put(item)
print(f'生产:{item}')
time.sleep(1)
def consumer(queue):
while True:
item = queue.get()
print(f'消费:{item}')
queue.task_done()
time.sleep(1.5)
if __name__ == '__main__':
q = Queue()
t1 = threading.Thread(target=producer, args=(q,))
t2 = threading.Thread(target=consumer, args=(q,))
t2.daemon = True # 线程2是无限循环需要设置守护线程以便主线程退出
t1.start()
t2.start()
t1.join() # 等待所有项被生产
q.join() # 等待所有项被消费
这个代码有三个值得注意的地方:
t2.daemon = True
q.join()
t1.join()
1. 为什么消费者线程 t2
是守护线程?
在一般的生产者-消费者模型中,生产者和消费者之间是独立的。这意味着消费者不知道生产者总共有多少东西需要生产。所以,为了顺利收到生产者生产的所有数据,消费者会有一个死循环(while True:
),一直等待生产者的数据。
但是,这样又会出现一个问题,主线程停止前,必须等待所有非守护线程的子进程结束任务,然而我们的消费者进程是一个死循环,无法自己结束任务,这就会卡死主线程。所以,消费者必须要设置为守护线程,这样才能让主线程不等待消费者线程结束任务,从而让主线程正常退出。
2. 为什么要 q.join()
?
从上面的说法中我们很发现,主线程会不等待身为守护线程的消费者线程,直接退出。此时,万一队列 q
中生产者生产的数据,就直接丢了……所以,在主线程结束前必须加上限制,这个限制就是让队列中没有数据后,再退出主线程,此时消费者线程一定接收了所有数据。
3. 为什么要 t1.join()
?
上面两点补充完后,看起来已经很好了,但是还有个问题会发生。
假如生产者比消费者更慢,生产者刚给出数据就被消费者使用。这个时候,因为 q.join()
的原理大致是看什么时候 q
中为空就算通过,所以说在这种情况下,生产者刚毅生产出来就被消费者拿下。此时 q
中正好为空,也就直接通过该限制了……这又回到了老路上,主线程不会等待身为守护线程的消费者线程,直接退出……
要想避免这一点,就需要在 q.join()
前面首先加上身为生产者的限制 t1.join()
,这意味着主线程首先要等待生产者线程执行完毕才能继续看队列是否为空,此时生产者线程工作完毕,队列如果为空,就一定是消费者都获取到了数据,此时主线程结束也就是正确的了。
总结
- 在所有有死循环的线程上,加守护线程标志
- 先判断生产完数据(
t1.join()
),然后判断消费完数据(q.join()
)