本文深度对比 Python 并发方案适用场景和优缺点,主要是介绍 asyncio 这个方案。
注:本文代码需要使用 Python 3.10 及以上版本才能正常运行。
在 Python 世界有 3 种并发和并行方案,如下:
注:并发和并行的区别先不提,最后会借着例子更好的解释,另外稍后也会提到concurrent.futures
,不过它不是一种独立的方案,所以在这里没有列出来。
这些方案是为了解决不同特点的性能瓶颈。性能问题主要有 2 种:
如果你不知道一个任务哪种类型,我的经验是你问问自己,如果给你一个更好更快的 CPU 它可以更快,那么这就是一个 CPU 密集的任务,否则就是 I/O 密集的任务。
这三个方案中对于 CPU 密集型的任务,优化方案只有一种,就是使用多进程充分利用多核 CPU 一起完成任务,达到提速的目的。而对于 I/O 密集型的任务,则这三种方案都可以。
接着借着一个抓取网页并写入本地 (典型的 I/O 密集型任务) 小例子来挨个拆解对比一下这些方案。先看例子:
import requests url = 'https://movie.douban.com/top250?start=' headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36' # noqa } def fetch(session, page): with (session.get(f'{url}{page*25}', headers=headers) as r, open(f'top250-{page}.html', 'w') as f): f.write(r.text) def main(): with requests.Session() as session: for p in range(25): fetch(session, p) if __name__ == '__main__': main() |
在这个例子中会抓取豆瓣电影 Top250 的 25 个页面 (每页显示 10 个电影),使用 requests 库,不同页面按顺序请求,一共花了 3.9 秒:
➜ time python io_non_concurrent.py python io_non_concurrent.py 0.23s user 0.05s system 7% cpu 3.911 total |
这个速度虽然看起来还是很好的,一方面是豆瓣做了很好的优化,一方面我家的带宽网速也比较好。接着用上面三种方案优化看看效果。
多进程版本
Python 解释器使用单进程,如果服务器或者你的电脑是多核的,这么用其实是很浪费的,所以可以通过多进程提速:
from multiprocessing import Pool def main(): with (Pool() as pool, requests.Session() as session): pool.starmap(fetch, [(session, p) for p in range(25)]) |
注:这里省略到了那些上面已经出现的了代码,只展示改变了的那部分。
使用多进程池,但没指定进程数量,所以会按着 Macbook 的核数启动 10 个进程一起工作,耗时如下:
➜ time python use_multiprocessing.py python use_multiprocessing.py 2.15s user 0.30s system 232% cpu 1.023 total |
多进程理论上可以有十倍效率的提升,因为 10 个进程在一起执行任务。当然由于任务数量是 25,不是整数倍,是无法达到 10 倍的降低耗时,而且由于抓取太快了,没有充分显示多进程方案下的效率提升,所以用时 1 秒,也就是大约 4 倍的效率提升。
多进程方案下没有明显的缺点,只要机器够强悍,就可以更快。
多线程版本
Python 解释器不是线程安全的,为此 Python 设计了 GIL: 获得 GIL 锁才可以访问线程中的 Python 对象。所以在任何一个时间,只有一个线程可以执行代码,这样就不会引发竞态条件 (Race Conditio