Python 队列queue与多线程组合(生产者+消费者模式)

Python025

Python 队列queue与多线程组合(生产者+消费者模式),第1张

在线程世界⾥,⽣产者就是⽣产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果⽣产者处理速度很快,⽽消费者处理速度很慢,那么⽣产者就必须等待消费者处理完,才能继续⽣产数据。同样的道理,如果消费者的处理能⼒⼤于⽣产者,那么消费者就必须等待⽣产者。为了解决这个问题于是引⼊了⽣产者和消费者模式。

⽣产者消费者模式是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒。

比如,对于同时爬取多个网页的多线程爬虫,在某一时刻你可能无法保证他们在处理不同的网站,在某些时刻他们极有可能在处理相同的网站,这岂不浪费?为了解决这个问题,可以将不同网页的url放在queue中,然后多个线程来读取queue中的url进行解析处理,而queue只允许一次出一个,出一个少一个。相同网站上不同网页的url通常有某种规律,比如某个字段的数字加1,这种情况完全可以用这种模式,“生产者程序”负责根据规律把完整的url制作出来,再塞进queue里面(如果queue满了,则等待);“消费者程序(网页解析程序)”从queue的后面挨个取出url进行解析(如果queue里面是空的,则等待),即使是多线程也能保证每个线程得到的是不同的url。这个过程中,生产者和消费彼此互不干涉。

下面以实例说明如何将queue与多线程相结合形成所谓的“ 生产者+消费者 ”模式,同时解决 多线程如何退出 的问题(注意下例中是“一个生产者+多个消费者”的形式,多生产者+多消费者的模式可在此基础上进一步实现):

上述程序的过程如下图:

注意

(1)上述程序中生产者插入queue的时间间隔为0.1s,而消费者的取出时间间隔为2s,显然消费速度不如生产速度,一开始queue是空的,一段时间后queue就变满了,输出结果正说明了这一点。如果将两个时间调换,则结果相反,queue永远不会满,甚至只有1个值,因为只要进去就被消费了。

(2)消费者程序是通过“while”来推动不断执行的,何时结束?上例中通过在queue中增加None的形式告诉消费者,生产者已经结束了,消费者也可以结束了。但消费者有多个,到底由哪个消费者得到None?为解决这个问题,上例中在消费者中先判断当前取出的是不是None,如果是,则先在queue里插入一个None,然后再break当前这个消费者线程,最后的结果是所有的消费者线程都退出了,但queue中还剩下None没有被取出。因此在程序的后面增加了一个for循环来挨个把queue中的元素取出,否则最后的q.join()将永远阻塞,程序无法往下执行。

(3)程序中每一个q.get()后面都跟有一个q.task_done(),其作用是从queue中取出一个元素就给q.join()发送一个信息,否则q.join()将永远处于阻塞状态,直到所有queue元素都被取出。

多线程“生产者-消费者”模式一般性结构图

关于 的生产者和消费者的实现,刚好最近有用到,简单总结记录下:

是系统独立调度核分配系统资源(CPU、内存)的基本单位,进程之间是相互独立的,每启动一个新的进程相当于把数据进行了一次克隆。

python提供了多种方法实现了多进程中间的 (可以修改同一份数据)。

GIL 的全称是 Global Interpreter Lock(全局解释器锁),来源是 Python 设计之初的考虑,为了数据安全所做的决定。

某个线程想要执行,必须先拿到 GIL,我们可以把 GIL 看作是“通行证”,并且在一个 Python 进程中,GIL 只有一个,这就导致了多线程抢占GIL耗时。这就是为什么在多核CPU上,Python 的多线程效率并不高的根本原因。

所以有必要学习下多进程的使用。