在python3中使用异步方法处理并发
随着python 3.7版本的正式发布,async/await终于也变成了关键字,从python 3.5引入这个新语法以来,已经过了3年,python异步编程也逐渐变成了重要的概念。
之前我一般使用线/进程池来处理并发,现在是时候学习一下标准库的异步框架了。
1. 传统的同步方法处理IO问题
由于python中GIL锁的存在,传统的计算密集型事务一般采用多进程模型,而IO密集型则多用相对轻量一点多线程模型。例如常见的一种场景,爬虫大批量下载内容的时候,线程池是一种常用的技术。
下面先用普通的同步方法下载一组图片,这里简单起见使用wallhaven上的图片,一方面是因为它的url定义很简单,另一方面这个站点在我这里连接比较慢,容易体现出各种方法的差异性,简单起见,实现下载的函数使用了requests库。(注:2020年,wallhaven已经更改了链接样式,请使用其它站点或方法测试。)
# -*- coding: utf-8 -*-
# 演示用代码,简单起见假设路径都已经存在
=
"""根据图片id下载,使用同步方法,不验证ssl以节省时间."""
= f
=
return
= f
=
"""按顺序同步下载所有图片."""
在网络环境稳定的情况下,使用这种方法下载了49张图片,多次平均之后花费时间是42.9s。
2. 多线/进程处理IO的并发问题
这里简单起见就直接使用线程池了,当然进程池效率区别也不大,sync_fetch_img函数沿用上面的,主要原理就是在线程池中取出线程并发地发送请求。
"""使用进/线程池下载所有图片."""
return
# 当max_worker设置为None时,线程池的worker数目使用处理器数目乘以5,进程池为处理器数
在5个线程的情况下,下载时间的平均值为22.9s。
3. 基于标准库asyncio的异步处理方法
asyncio实际上是由python中的yield一步一步演化来的,先用asyncio配合aiohttp这个异步网络库试一试,主要就是异步发送请求,无需等待,异步的获取结果这种过程。
"""根据图片id下载,使用异步方法."""
= f
# 同理不验证ssl
return
= f
=
# 流式下载分块写入相对稳妥一点
= await
break
这里实现了一个基本的异步下载函数,关于它的使用方法就是把它放到事件循环里执行,写法相当简洁,如下所示:
# 使用uvloop比默认的事件循环效率高一点
=
在这种异步处理的情况下,下载时间的平均值为22.9s。
4. 关于结果的分析
第一种方案里同步获取,图片一张一张地下载,效率较低;第二种方案里实际上启动了5个线程,理论上速度会接近第一种的5倍,但实际上只是2倍左右,查看当时网络统计,发现是因为测试环境的小水管带宽倍跑慢了,制约了速度的上限;同理,第三种也是受到了网络带宽的约束,因此耗时与第二种相同。
下次需要找个更好的网络环境再测一遍。