线程局限

  1. 多线程在运行过程中容易被打断,有可能会出现 race condition 的情况
  2. 线程切换本身存在开销,不能无限增加线程数

Sync vs Async

  1. Sync - 操作顺序执行,前面阻塞后面
  2. Async - 不同操作间可以交替执行,如果其中一个操作被阻塞了,程序不会等待,而是会找出可执行的操作继续执行

原理

CSP - Communicating sequential processes

  1. asyncio 与 Python 程序一样,都是单线程
  2. asyncio 只有一个主线程,但可以进行多个不同的任务(特殊的 Future 对象),这些不同的任务被 Event loop 控制
  3. 假设任务只有两个状态 - 预备状态 / 等待状态
    • 预备状态 - 任务目前空闲,随时准备运行
    • 等待状态 - 任务已经运行,但在等待外部操作完成(如 IO)
  4. Event loop 维护两个任务列表,分别对应预备状态和等待状态
    • 选取预备状态的一个任务(与任务的等待时长、占用资源等相关),使其运行,直到该任务将控制权交还给 Event loop 为止
      • 当任务将控制权交还给 Event loop 时,Event loop 会根据其是否完成,将任务放到预备状态列表或者等待状态列表
    • 然后遍历等待状态列表的任务,查看是否完成
      • 如果完成,将其放到预备状态的列表
      • 如果未完成,继续放在等待状态的列表
    • 原先在预备状态列表的任务位置仍旧不变,是因为它们还未运行
  5. asyncio 的任务运行时不会被外部的一些因素打断,因此不会出现 race condition,无需担心线程安全的问题 - CSP
    • IO 密集的场景下,asyncio多线程运行效率更高
      • 因为 asyncio 内部任务切换的开销,远低于线程切换的开销
    • 并且 asyncio 可以开启非常多的任务数量 - 类似于 Goroutine

用法

image-20241101170155588

  1. async with 是 asyncio 的最新写法 - 表示该语句/函数是 non-blocking
  2. 如果任务执行的过程中需要等待,则将该任务放入等待状态列表中,然后继续执行预备状态列表中的任务
  3. asyncio.run 是 asyncio 的 root call,表示拿到 Event loop,运行任务,直到结束,最后关闭该 Event loop
1
2
tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*tasks)
  1. asyncio.create_task(coro) 表示对协程 coro 创建一个任务(特殊的 Future 对象),安排其执行
  2. asyncio.gather 表示在 Event loop 中运行所有任务

缺陷

  1. 要发挥 asyncio 的能力,需要对应的 Python 库支持
    • requests 不支持 asyncio,而 aiohttp 支持
  2. 使用 asyncio,在任务调度方面有更大的自主权,但容易出错

多线程 vs Async IO

asyncio 是单线程,但其内部的 Event loop 机制,可以并发地运行多个不同的任务,且比多线程更自主

1
2
3
4
5
6
7
if io_bound:
if io_slow:
print('Use Asyncio')
else:
print('Use multi-threading')
else if cpu_bound:
print('Use multi-processing')
  1. 对于 CPU 密集型任务,使用多线程无效的 - 由于 GIL 的限制 - 请使用多进程
    • Python 多线程的本质 - 多个线程互相切换,但由于 GIL 的限制,在同一时刻仍然只允许一个线程运行
      • 使用多线程和使用单一主线程,对于 CPU 密集型任务来说,本质上来说没有区别
      • 反而在很多情况下,因为线程切换带来的额外损耗会降低程序性能
    • 使用多进程,是允许多个进程间并行的,能提高程序性能
  2. 对于 IO 密集型任务,如果想要加速,优先选择多线程或者 asyncio,因为瓶颈在 IO 上,而非 CPU 上
    • 使用多进程也行,但完全没有必要
    • asyncio 可以支持比多线程更多的连接数