Python - Async IO
线程局限
- 多线程在运行过程中容易被打断,有可能会出现 race condition 的情况
- 线程切换本身存在开销,不能无限增加线程数
Sync vs Async
- Sync - 操作顺序执行,前面阻塞后面
- Async - 不同操作间可以交替执行,如果其中一个操作被阻塞了,程序不会等待,而是会找出可执行的操作继续执行
原理
CSP - Communicating sequential processes
- asyncio 与 Python 程序一样,都是单线程的
- asyncio 只有一个主线程,但可以进行多个不同的任务(特殊的 Future 对象),这些不同的任务被 Event loop 控制
- 假设任务只有两个状态 - 预备状态 / 等待状态
- 预备状态 - 任务目前空闲,随时准备运行
- 等待状态 - 任务已经运行,但在等待外部操作完成(如 IO)
- Event loop 维护两个任务列表,分别对应预备状态和等待状态
- 选取预备状态的一个任务(与任务的等待时长、占用资源等相关),使其运行,直到该任务将控制权交还给 Event loop 为止
- 当任务将控制权交还给 Event loop 时,Event loop 会根据其是否完成,将任务放到预备状态列表或者等待状态列表
- 然后遍历等待状态列表的任务,查看是否完成
- 如果完成,将其放到预备状态的列表
- 如果未完成,继续放在等待状态的列表
- 原先在预备状态列表的任务位置仍旧不变,是因为它们还未运行
- 选取预备状态的一个任务(与任务的等待时长、占用资源等相关),使其运行,直到该任务将控制权交还给 Event loop 为止
- asyncio 的任务运行时不会被外部的一些因素打断,因此不会出现 race condition,无需担心线程安全的问题 - CSP
- 在 IO 密集的场景下,asyncio 比多线程的运行效率更高
- 因为 asyncio 内部任务切换的开销,远低于线程切换的开销
- 并且 asyncio 可以开启非常多的任务数量 - 类似于 Goroutine
- 在 IO 密集的场景下,asyncio 比多线程的运行效率更高
用法
async with
是 asyncio 的最新写法 - 表示该语句/函数是 non-blocking- 如果任务执行的过程中需要等待,则将该任务放入等待状态列表中,然后继续执行预备状态列表中的任务
- asyncio.run 是 asyncio 的 root call,表示拿到 Event loop,运行任务,直到结束,最后关闭该 Event loop
1 | tasks = [asyncio.create_task(download_one(site)) for site in sites] |
asyncio.create_task(coro)
表示对协程 coro 创建一个任务(特殊的 Future 对象),安排其执行asyncio.gather
表示在 Event loop 中运行所有任务
缺陷
- 要发挥 asyncio 的能力,需要对应的 Python 库支持
- requests 不支持 asyncio,而 aiohttp 支持
- 使用 asyncio,在任务调度方面有更大的自主权,但容易出错
多线程 vs Async IO
asyncio 是单线程,但其内部的 Event loop 机制,可以并发地运行多个不同的任务,且比多线程更自主
1 | if io_bound: |
- 对于 CPU 密集型任务,使用多线程是无效的 - 由于 GIL 的限制 - 请使用多进程
- Python 多线程的本质 - 多个线程互相切换,但由于 GIL 的限制,在同一时刻仍然只允许一个线程运行
- 使用多线程和使用单一主线程,对于 CPU 密集型任务来说,本质上来说没有区别
- 反而在很多情况下,因为线程切换带来的额外损耗会降低程序性能
- 使用多进程,是允许多个进程间并行的,能提高程序性能
- Python 多线程的本质 - 多个线程互相切换,但由于 GIL 的限制,在同一时刻仍然只允许一个线程运行
- 对于 IO 密集型任务,如果想要加速,优先选择多线程或者 asyncio,因为瓶颈在 IO 上,而非 CPU 上
- 使用多进程也行,但完全没有必要
- asyncio 可以支持比多线程更多的连接数
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.