python 多进程并发:生产者+多消费者模式
多任务场景中,为了节省大量子任务串行执行的耗时,通常采用并发方式充分利用 cpu 和 内存来节省整体任务运行时间。
对于多任务并发,常见的做法自然是抽象出功能函数,借助 multiprocess 类在主进程中并发出多个子进程,或者构建进程池,将任务构造好后丢入进程池中来实现并发。这种方式对于逻辑结构较为简单的场景来说实现方便,稳定性也有保证,但是对于逻辑更为复杂的场景来说,会造成代码耦合,不够优雅~
其实从设计模式的角度来看,很多多任务场景都可以抽象成 生产者-消费者 模型,各个具体任务的区别无非采用 多生产者、多消费者、单生产者+单消费者、单生产者+多消费者、多生产者+单消费者、多生产者+多消费者中的哪一种模式。
现在常用的基于消息队列的分布式任务调度/消费系统,其实从宏观上来看就可以把它理解成一个 单/多生产者+多消费者 模式。这种模式下,生产者构建具体任务,并将构建好的任务放入任务队列中,然后并发的多个消费者都从任务队列中取任务来执行,消息队列如果放在单台机器上,那就是普通的多任务并发,放在多台机器上,就具有了分布式的雏形。
1 单生产者+多消费者
由于工作场景需要,需要通过 python
实现一个 单生产者+多消费者 模式,其中通过 Queue
实现消息共享来串联起生产和消费。特此记录下,既方便自己查询,也能够以飨来者。由于 python
全局锁 GIL
的原因,导致在 python
中线程并不能完全的实现并行,只能实现并发,所以代码中通过多进程的方式来实现并行。除此之外,对生产者和消费者模块的抽象是通过继承 Process
类并重写 run()
函数的方式来实现的。
首先看极简的抽象代码:
# 导入相关依赖
import time
import random
import multiprocessing
from multiprocessing import JoinableQueue
生产者:
class Producer(multiprocessing.Process):
"""
生产者:制作淀粉肠
"""
def __init__(self, t_name, queue):
multiprocessing.Process.__init__(self, name=t_name)
self.queue = queue # 任务队列
self.t_name = t_name
def run(self):
# 生产任务(生产 20 根淀粉肠)
for i in range(20):
print(self.t_name, "生产了一根淀粉肠", i)
self.queue.put(i) # 把淀粉肠放入队列
print("所有淀粉肠生产完毕~")
self.queue.join()
print("队列被拿完了~")
消费者:
def func(name, var):
"""
具体的消费任务逻辑
"""
print(name, "吃了一根淀粉肠", var)
time.sleep(random.randint(5, 10)) # 模拟耗时操作
return 'success'
class Consumer(multiprocessing.Process):
"""
消费者:消费淀粉肠
"""
def __init__(self, t_name, queue):
multiprocessing.Process.__init__(self, name=t_name)
self.queue = queue # 任务队列
self.t_name = t_name
def run (self):
while True:
var = self.queue.get()
func(self.name, var) # 模拟耗时消费操作
self.queue.task_done() # 发送一次信号,证明一个数据已经被取走
主函数:
if __name__ == '__main__':
workers = 16 # 消费者数量
# 任务队列(设定最大长度为 16,队列满时生产者会等待,直到队列有空间再往里写新的任务)
q = JoinableQueue(maxsize=16)
# 生产者
producer = Producer("生产者", q)
producer.start()
# 多个消费者
for i in range(workers):
consumer = Consumer(f"消费者 {i}", q)
consumer.daemon = True # 将消费者进程设置为主进程的守护进程,这样当主进程结束后其会自动终止
consumer.start()
# 调用 join,作用是等到生产者进程执行结束后再让主进程继续往下执行(这样就能保证所有任务都被构造)
producer.join()
注意,上面代码使用了 JoinableQueue
而不是一般的 Queue
,主要是因为 JoinableQueue
具有 q.task_done()
函数,结合 JoinableQueue.join()
能帮我们实现监控队列中所有任务都消费完后的情况,避免主进程结束引起消费者守护进程结束而导致队列中有些任务没有被消费完。读者可以自己注释 Productor
中的 self.queue.join()
然后看看打印结果比较下生产者和消费者的输出数量。
2 消费者结束逻辑优化
上面代码实现通过在消费者中通过 while True
循环持续监听队列,只有当主进程结束之后消费者进程才会结束。如果是一次性任务或者是长时监听场景自然没什么问题。但如果是定时调度场景,并且任务种类是动态变化的,主进程不会结束但会反复调用生产者和消费者,此时部分消费者进程可能会无法及时关闭进而形成僵尸进程。
为此,需要设计一种方式来使得消费者能在任务拿完之后退出消费。
方案:在生产者构造完所有任务后,再次向队列中放入 N
(等于消费者进程数)个特殊的标识(例如 None
),然后消费者从队列中拿取时,一但拿到的是这个特殊标识,就退出循环。
修改后的消费者:
class Consumer(multiprocessing.Process):
"""
消费者:消费淀粉肠
"""
def __init__(self, t_name, queue):
multiprocessing.Process.__init__(self, name=t_name)
self.queue = queue # 任务队列
self.t_name = t_name
def run (self):
while True:
var = self.queue.get() # 从队列中取出一根淀粉肠
if var is None:
break
else:
func(self.name, var) # 模拟耗时消费操作
self.queue.task_done() # 发送一次信号,证明一个数据已经被取走
主函数:
if __name__ == '__main__':
workers = 16 # 消费者数量
# 任务队列(设定最大长度为 16,队列满时生产者会等待,直到队列有空间再往里写新的任务)
q = JoinableQueue(maxsize=16)
# 生产者
producer = Producer("生产者", q)
producer.start()
# 多个消费者
consumer_list = []
for i in range(workers):
consumer = Consumer(f"消费者 {i}", q)
# consumer.daemon = True # 将消费者进程设置为主进程的守护进程,这样当主进程结束后其会自动终止
consumer.start()
consumer_list.append(consumer)
# 调用 join,作用是等到生产者进程执行结束后再让主进程继续往下执行(这样就能保证所有任务都被构造)
producer.join()
# 写入特殊标识
for i in range(workers):
q.put(None)
for consumer in consumer_list:
consumer.join()
# 验证各子进程状态
print('-' * 90)
print('productor:', producer.is_alive())
for i, consumer in enumerate(consumer_list):
print(f'consumer {i}:', consumer.is_alive())
print('-' * 90)
其余部分代码和上面相同。
参考:
- https://blog.csdn.net/weixin_45889256/article/details/133277198
- https://blog.csdn.net/qq_37310110/article/details/129587183
- https://zhuanlan.zhihu.com/p/545949227
- https://zhuanlan.zhihu.com/p/586030177
- https://www.cnblogs.com/dong-/p/9520659.html
- https://docs.python.org/3/library/threading.html#threading.Event
- https://www.cnblogs.com/mike-liu/p/9271875.html【理解 join】
- https://www.modb.pro/db/144323
本文来自博客园,作者:sinatJ,转载请注明原文链接:https://www.cnblogs.com/zishu/p/18087651