Python爬虫的经典多线程方式,生产者与消费者模型
在之前的文章当中我们曾经说道,在多线程并发的场景当中,如果我们需要感知线程之间的状态,交换线程之间的信息是一件非常复杂和困难的事情。因为我们没有更高级的系统权限,也没有上帝视角,很难知道目前运行的状态的全貌,所以想要设计出一个稳健运行没有bug的功能,不仅非常困难,而且调试起来非常麻烦。
很多人学习python,不知道从何学起。
很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手。
很多已经做案例的人,却不知道如何去学习更加高深的知识。
那么针对这三类人,我给大家提供一个好的学习平台,免费领取视频教程,电子书籍,以及课程的源代码!
QQ群:1097524789
生产消费者模式
在日常开发当中, 从一个线程向另外的线程传输数据又是一件家常便饭的事情 。举个最简单的例子,我们在处理网页请求的时候,需要打印下来这一次请求的相关日志。打印日志是一次IO行为,这是非常消耗时间的,所以我们不能放在请求当中同步进行,否则会影响系统的性能。最好的办法就是启动一系列线程专门负责打印,后端的线程只负责响应请求,相关的日志以消息的形式传送给打印线程打印。
这个简单的不能再简单的功能当中涉及了诸多细节,我们来盘点几个。首先IO线程的数据都是从后台线程来的,假如一段时间内没有请求,那么这些线程都应该休眠,应该在有请求的时候才会启动。其次,如果某一段时间内请求非常多,导致IO线程一时间来不及打印所有的数据,那么当下的请求应该先暂存起来,等IO线程”忙过来“之后再进行处理。
把这些细节都考虑到,自己来设计功能还是挺麻烦的。好在这个问题前人已经替我们想过了,并且得出了一个非常经典的设计模式,使用它可以很好的解决这个问题。这个模式就是 生产消费者模式 。
这个设计模式的原理其实非常简单,我们来看张图就明白了。
Java并发-- 生产者-消费者模式| 点滴积累
线程根据和数据的关系分为 生产者线程和消费者线程 ,其中生产者线程负责生产数据,产生了数据之后会存储到任务队列当中。消费者线程从这个队列获取需要消费的数据,它和生产者线程之间不会直接交互,避免了线程之间互相依赖的问题。
另外一个细节是这里的任务队列并不是普通的队列,一般情况下是一个 阻塞队列 。也就是说当消费者线程尝试从其中获取数据的时候,如果队列是空的,那么这些消费者线程会自动挂起等待,直到它获得了数据为止。有阻塞队列当然也有非阻塞队列,如果是非阻塞队列的话,当我们尝试从其中获取数据的时候,如果它当中没有数据的话,并不会挂起等待,而是会返回一个空值。
当然阻塞队列的挂起等待时间也是可以设置的,我们可以让它一直等待下去,也可以设置一个最长等待时间 。如果超过这个时间也会返回空,不同的队列应用在不同的场景当中,我们需要根据场景性质做出调整。
代码实现
看完了设计模式的原理,我们下面来试着用代码来实现一下。
在一般的高级语言当中都有现成的队列的库,由于在生产消费者模式当中用到的是阻塞型queue,有阻塞性的队列当然也就有非阻塞型的队列。我们在用之前需要先了解清楚,如果用错了队列会导致整个程序出现问题。在Python当中,我们最常用的queue就是一个 支持多线程场景的阻塞队列 ,所以我们直接拿来用就好了。
由于这个设计模式非常简单,这个代码并不长只有几行:
from queue import Queue
from threading import Thread
def producer(que):
data = 0
while True:
data += 1
que.put(data)
def consumer(que):
while True:
data = que.get()
print(data)
que = Queue()
t1 = Thread(target=consumer, args=(que, ))
t2 = Thread(target=producer, args=(que, ))
t1.start()
t2.start()
我们运行一下就会发现它是可行的,并且由于队列 先进先出 的限制,可以保证了consumer线程读取到的内容的 顺序和producer生产的顺序是一致的 。
如果我们运行一下这个代码会发现它是不会结束的,因为consumer和producer当中都用到了while True构建的死循环,假设我们希望可以控制程序的结束,应该怎么办?
其实也很简单,我们也可以利用队列。我们创建一个特殊的信号量,约定好当consumer接受到这个特殊值的时候就停止程序。这样当我们要结束程序的时候,我们只需要把这个信号量加入队列即可。
singal = object()
def producer(que):
data = 0
while data < 20:
data += 1
que.put(data)
que.put(singal)
def consumer(que):
while True:
data = que.get()
if data is singal:
# 继续插入singal
que.put(singal)
break
print(data)
这里有一个细节是我们在consumer当中,当读取到singal的时候,在跳出循环之前我们又把singal放回了队列。原因也很简单,因为有时候consumer线程不止一个,这个singal上游 只放置了一个,只会被一个线程读取进来 ,其他线程并不会知道已经获得了singal的消息,所以还是会继续执行。
而当consumer关闭之前放入singal就可以保证每一个consumer在关闭的之前都会再传递一个结束的信号给其他未关闭的consumer读取。这样一个一个的传递,就可以保证所有consumer都关闭。
这里还有一个小细节,虽然利用队列可以解决生产者和消费者通信的问题,但是上游的生产者并不知道下游的消费者是否已经执行完成了。假如我们想要知道,应该怎么办?
Python的设计者们也考虑到了这个问题,所以他们在Queue这个类当中加入了 task_done和join方法 。利用task_done,消费者可以通知queue这一个任务已经执行完成了。而通过调用join,可以等待所有的consumer完成。
from queue import Queue
from threading import Thread
def producer(que):
data = 0
while data < 20:
data += 1
que.put(data)
def consumer(que):
while True:
data = que.get()
print(data)
que.task_done()
que = Queue()
t1 = Thread(target=consumer, args=(que, ))
t2 = Thread(target=producer, args=(que, ))
t1.start()
t2.start()
que.join()
除了使用task_done之外,我们还可以在que传递的消息当中加入一个Event,这样我们还可以继续感知到每一个Event执行的情况。
优先队列与其他设置
我们之前在介绍一些分布式调度系统的时候曾经说到过,在调度系统当中,调度者会用一个优先队列来管理所有的任务。当有机器空闲的时候,会有限调度那些优先级高的任务。
其实这个调度系统也是基于我们刚才介绍的生产消费者模型开发的,只不过 将调度队列从普通队列换成了优先队列 而已。所以如果我们也希望我们的consumer能够根据任务的优先级来改变执行顺序的话,也可以使用优先队列来进行管理任务。
关于优先队列的实现我们已经很熟悉了,但是有一个问题是我们需要实现挂起等待的阻塞功能。这个我们自己实现是比较麻烦的,但好在我们可以通过调用相关的库来实现。比如threading中的Condition, Condition是一个条件变量可以通知其他线程,也可以实现挂起等待 。
from threading import Thread, Condition
class PriorityQueue:
def __init__(self):
self._queue = []
self._cv = Condition()
def put(self, item, priority):
with self._cv:
heapq.heappush(self._queue, (-priority, self._count, item))
# 通知下游,唤醒wait状态的线程
self._cv.notify()
def get(self):
with self._cv:
# 如果对列为空则挂起
while len(self._queue) == 0:
self._cv.wait()
# 否则返回优先级最大的
return heapq.heappop(self._queue)[-1]
最后介绍一下Queue的其他设置,比如我们可以 通过size参数设置队列的大小 ,由于这是一个阻塞式队列,所以如果我们设置了队列的大小,那么当队列被装满的时候,往其中插入数据的操作也会被阻塞。此时producer线程会被挂起,一直到队列不再满为止。
当然我们也可以通过block参数 将队列的操作设置成非阻塞 。比如que.get(block=False),那么当队列为空的时候,将会抛出一个队列为空的异常。同样,que.put(data, block=False)时也一样会得到一个队列已满的异常。
总结
今天这篇文章当中我们主要介绍了多线程场景中经典的生产消费者模式,这个模式在许多场景当中都有使用。比如kafka等消息系统,以及yarn等调度系统等等,几乎只要是涉及到多线程上下游通信的,往往都会用到。也正因此它的使用场景太广了,所以它 经常在各种面试当中出现 ,也可以认为是工程师必须知道的几种基础设计模式之一。
另外,队列也是一个在设计模式以及使用场景当中经常出现的数据结构。从侧面也说明了,为什么算法和数据结构非常重要,许多大公司喜欢问一些算法题,也是因为 有实际的使用场景 ,并且的的确确能锻炼工程师的思维能力。经常有同学问我算法和数据结构的使用案例,这就是一个很好的例子。
Python爬虫的经典多线程方式,生产者与消费者模型的更多相关文章
- 【java线程系列】java线程系列之线程间的交互wait()/notify()/notifyAll()及生产者与消费者模型
关于线程,博主写过java线程详解基本上把java线程的基础知识都讲解到位了,但是那还远远不够,多线程的存在就是为了让多个线程去协作来完成某一具体任务,比如生产者与消费者模型,因此了解线程间的协作是非 ...
- python多线程+生产者和消费者模型+queue使用
多线程简介 多线程:在一个进程内部,要同时干很多事情,就需要同时执行多个子任务,我们把进程内的这些子任务叫线程. 线程的内存空间是共享的,每个线程都共享同一个进程的资源 模块: 1._thread模块 ...
- Python之生产者&、消费者模型
多线程中的生产者和消费者模型: 生产者和消费者可以用多线程实现,它们通过Queue队列进行通信. import time,random import Queue,threading q = Queue ...
- JAVA之旅(十五)——多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止
JAVA之旅(十五)--多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止 我们接着多线程讲 一.生产者和消费者 什么是生产者和消费者?我们 ...
- python queue和生产者和消费者模型
queue队列 当必须安全地在多个线程之间交换信息时,队列在线程编程中特别有用. class queue.Queue(maxsize=0) #先入先出 class queue.LifoQueue(ma ...
- 人生苦短之我用Python篇(队列、生产者和消费者模型)
队列: queue.Queue(maxsize=0) #先入先出 queue.LifoQueue(maxsize=0) #last in fisrt out queue.PriorityQueue( ...
- Python 之并发编程之进程下(事件(Event())、队列(Queue)、生产者与消费者模型、JoinableQueue)
八:事件(Event()) # 阻塞事件: e = Event() 生成事件对象e e.wait() 动态给程序加阻塞,程序当中是否加阻塞完全取决于该对象中的is_set() [默认返回值 ...
- 母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列
简介 多线程通信一直是高频面试考点,有些面试官可能要求现场手写生产者/消费者代码来考察多线程的功底,今天我们以实际生活中母鸡下蛋案例用代码剖析下实现过程.母鸡在鸡窝下蛋了,叫练从鸡窝里把鸡蛋拿出来这个 ...
- python并发编程之守护进程、互斥锁以及生产者和消费者模型
一.守护进程 主进程创建守护进程 守护进程其实就是'子进程' 一.守护进程内无法在开启子进程,否则会报错二.进程之间代码是相互独立的,主进程代码运行完毕,守护进程也会随机结束 守护进程简单实例: fr ...
随机推荐
- java 面向对象(四十二):反射(六)反射应用三:调用运行时类的指定结构
调用指定的属性: @Test public void testField1() throws Exception { Class clazz = Person.class; //创建运行时类的对象 P ...
- python 装饰器(五):装饰器实例(二)类装饰器(类装饰器装饰函数)
回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象(不太严谨,详见后文). 那么用类来实现也是也可以的.我们可以让类的构造函数__init__()接受一个函 ...
- How to use the function of bind
The usage of bind is to define a specified scope for called function. Because the key this is easy ...
- ShaderLab-坐标转换
观察空间就是相机的空间 投影矩阵本质就是对x.y.z分量进行不同程度的缩放(z还做了平移),结果就是视锥体近切面远切面变成正方形.视锥体的中心在(0,0). (对于正交相机,这一步已经得到了立方体) ...
- node.js02 安装Node环境
安装Node环境 在node.js01中我大概了解了什么是node.js,这次进入起步阶段,首先要安装下Node环境. 开始安装 查看当前Node环境的版本号 win+r输入cmd进入命令行,输入no ...
- 题解 CF1354B 【Ternary String】
题意 给出一个字符串,只包含 \({1,2}\) 或 \({3}\) .从中找出一个长度最短的子串,要求至少包含 \({1,2,3}\) 各一次,并输出其长度. 输入格式 本题有多组测试数据 第一行一 ...
- 题解 CF786B 【Legacy】
本题要求我们支持三种操作: ① 点向点连边. ② 点向区间连边. ③ 区间向点连边. 然后跑最短路得出答案. 考虑使用线段树优化建图. 建两颗线段树,入树和出树,每个节点为一段区间的原节点集合.入树内 ...
- 统计M
链接:https://vjudge.net/problem/UVA-1586 题意:给出一分子化学式,包含C,N,O,H四种元素,求M 题解:这是字符串题.分为几种情况:第一种是一个原子:第二种是多原 ...
- Python编程无师自通PDF高清完整版免费下载|百度网盘
百度网盘:Python编程无师自通PDF高清完整版免费下载 提取码:cx73 内容介绍 畅销Python编程类入门书,美国亚马逊Kindle编程类排行榜榜一. 作者从文科毕业,通过自学编程转行为专业程 ...
- R语言 循环语句、分支语句和中止语句-控制流篇
for 循环 用法 for (n in m) expr 若n在m中则运行 expr while 循环 用法 while (condition) expr 当符合condition时运行expr rep ...