asyncio调度介绍
概述
Python 的 asyncio 库提供了一种基于事件循环的异步编程模型,其核心是无栈协程(Stackless Coroutine)。无栈协程具有以下特性:
- 单线程事件循环:
asyncio在单线程上创建一个事件循环,用于调度协程的执行。 - 协作式多任务:协程只有在主动交出控制权(如通过
await)时,事件循环才能调度其他协程。 - 异步等待:使用
await调用可调度的对象(如协程、Future、Task),并获取其结果。
事件循环
事件循环是 asyncio 的核心组件,负责管理和调度所有异步任务。其工作原理如下:
- 检查调度堆:将到期的定时任务移动到就绪堆。
- 处理 I/O 事件:通过
select系统调用监听 I/O 事件,将就绪的 I/O 任务移动到就绪堆。 - 执行就绪任务:从就绪堆中取出任务并执行。
事件循环通过不断重复上述步骤,实现异步任务的调度和执行。
调度的对象
在 asyncio 中,协程可以调度的对象包括以下几种:
- 协程(Coroutine):
- 使用
async def定义的函数,可以通过await暂停和恢复执行。 - 协程本身是一个可等待对象,但不能直接被调度,需要封装为
Task。
- 使用
- Future:
- 表示一个异步操作的最终结果。
- 通常由低级 API 使用,开发者更多使用
Task。
- Task:
- 是
Future的子类,封装了一个协程。 - 代表一个正在运行的协程,事件循环会调度其执行。
- 是
这些对象都可以通过 await 进行等待调用。
调度结构
事件循环通过两个核心数据结构管理协程任务:就绪堆和调度堆。
就绪堆(Ready Queue)
- 作用:存储当前可以立即执行的协程任务。
- 特点:
- 任务按 FIFO(先进先出)的顺序执行。
- 当协程遇到
await并暂停时,事件循环会从就绪堆中取出下一个任务执行。
- 加入时机:
- 使用
call_soon()或call_soon_threadsafe()时,协程会加入就绪堆。 - I/O 事件完成或定时任务到期时,相关任务会加入就绪堆。
- 使用
调度堆(Scheduled Heap)
- 作用:存储需要延迟执行的协程任务,通常用于定时任务。
- 特点:
- 任务按时间戳排序,堆顶的任务是下一个即将到期的任务。
- 事件循环会定期检查调度堆,将到期的任务移动到就绪堆。
- 加入时机:
- 使用
asyncio.sleep()或loop.call_later()等延迟操作时,协程会被添加到调度堆。
- 使用
调度时机
协程被添加到调度堆或就绪堆的常见方式包括:
asyncio.sleep():- 当协程调用
await asyncio.sleep(delay)时,事件循环会将协程封装为一个定时任务,并添加到调度堆中,等待delay时间后执行。
- 当协程调用
loop.call_later():- 通过
loop.call_later(delay, callback),事件循环会将回调函数封装为定时任务,并添加到调度堆中。
- 通过
loop.call_at():- 通过
loop.call_at(when, callback),事件循环会将回调函数封装为定时任务,并添加到调度堆中,在指定时间when执行。
- 通过
asyncio.create_task():- 使用
asyncio.create_task()创建任务后,任务会在下一次await时自动添加到就绪堆。
- 使用
- 协程嵌套调用:
- 当一个协程 A 中
await另一个协程 B 时,协程 A 会通过add_done_callback方法注册一个唤醒回调。当协程 B 完成时,事件循环会唤醒协程 A 并将其加入就绪堆。
- 当一个协程 A 中
总结
asyncio 的调度机制基于事件循环和协程,通过就绪堆和调度堆管理任务的执行顺序。其核心特点包括:
- 协作式调度:协程通过
await主动交出控制权,事件循环根据任务状态调度执行。 - 高效 I/O 处理:通过
select系统调用监听 I/O 事件,避免阻塞。 - 灵活的任务管理:支持定时任务、嵌套协程调用等多种调度方式。
理解 asyncio 的调度机制有助于编写高效、可维护的异步代码,充分利用 Python 的异步编程能力。
参考资料
- Python 官方文档: https://docs.python.org/3/library/asyncio.html
- Asyncio 源码: https://github.com/python/cpython/tree/main/Lib/asyncio