Featured image of post 协作式多任务:Python Asyncio 的核心

协作式多任务:Python Asyncio 的核心

了解 Python 的协作式多任务机制,以及它与传统线程的区别

协作式多任务:主动交替执行

与传统操作系统通过强制切换进程(抢占式多任务)不同,Python 的 asyncio 采用了一种完全不同的方法,称为协作式多任务

协作式多任务的工作原理

在协作式多任务中:

  • 任务会一直运行,直到它们通过 await 主动让出控制权
  • 没有自动的时间片切换或强制抢占
  • CPU 密集型任务必须手动让出控制权,否则会阻塞其他任务
  • 如果某个任务不配合,可能会阻塞整个系统

可以把它想象成一组人讨论问题,每个人都同意只在自然停顿时发言,然后让别人说话。当大家都遵守规则时,这种方式非常有效,但如果有人一直占用话语权,就会破坏整个系统。

抢占式 vs 协作式:关键区别

抢占式多任务(操作系统线程) 协作式多任务(Asyncio)
操作系统强制中断任务 任务主动让出控制权
任务随时可能被中断 任务会一直运行直到遇到 await
系统定时器触发上下文切换 等待操作时触发切换
适合 CPU 密集型任务 最适合 I/O 密集型任务
需要复杂的同步机制 同步更简单
任务切换自动发生 程序员需手动添加 await 点

篮球类比

想象一场篮球比赛:

  • 抢占式多任务 就像有一个投篮计时器——时间一到,无论你在做什么,裁判(操作系统)都会强制收回球权
  • 协作式多任务 则像是街头篮球,靠自觉——大家投完篮或无法推进时主动传球

第二种方式只要大家都遵守规则就很顺畅,但只要有一个自私的球员,比赛就会变得糟糕。

为什么 Python 选择协作式多任务

协作式多任务有几个优势:

  1. 简单 —— 不需要锁等复杂的同步原语
  2. 高效 —— 没有频繁的上下文切换带来的开销
  3. 可预测 —— 任务在明确的点让出控制权
  4. 单线程 —— 避免了许多线程相关的 bug 和竞态条件

缺点是程序员需要更加注意代码何时、何地让出控制权。

使用 Asyncio 时需要注意的事项

由于 asyncio 依赖于任务间的协作,以下实践非常重要:

  • 使用 aiofiles.open() 替代普通的 open(),避免阻塞事件循环
  • 进行 HTTP 操作时用 aiohttp 替代 requests
  • 延时操作时用 asyncio.sleep() 替代 time.sleep()
  • 在 CPU 密集型操作中定期让出控制权:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async def compute_intensive_task():
    result = 0
    for i in range(1_000_000):
        result += i * i
        
        # 每 10,000 次迭代让出一次控制权,允许其他任务运行
        if i % 10000 == 0:
            await asyncio.sleep(0)  # 睡眠 0 秒,仅用于让出控制权
            
    return result
  • 对于真正的 CPU 密集型任务,建议使用线程池:
1
2
3
async def run_in_thread(cpu_bound_function, *args):
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(None, cpu_bound_function, *args)

理解并尊重 asyncio 的协作本质,你就能构建出高效且并发性强的应用程序,而无需传统多线程的复杂性。