asyncio 是 Python 标准库提供的异步 I/O 框架,它的核心思想是在单线程内通过协作式多任务实现高并发。要理解它怎么工作,关键要搞懂三个核心概念:事件循环、协程对象、任务。
事件循环(Event Loop)
- 是什么:事件循环就是一个“一直跑的程序大管家”,它维护一个任务队列,不断取出任务执行,遇到需要等待的操作(如网络请求、文件读取)就挂起当前任务,切换到其他可执行的任务,等 I/O 完成后再把挂起的任务放回队列继续执行。
- 怎么理解:可以把它想象成一个单线程的服务员,给多桌客人点菜。他不会站在一桌等着厨房炒菜,而是点完菜立刻去服务下一桌,厨房叫号时再端菜给对应的客人。这样一个人也能同时服务多桌。
- 在代码中如何获得:
asyncio.run(main())会创建一个事件循环,运行你写的main协程。在main内部,await语句就是“我这边要等一会儿,你先去干别的”。
协程对象(Coroutine Object)
- 是什么:用
async def定义的函数调用后返回的不是普通函数的返回值,而是一个协程对象。协程对象代表一段可暂停、可恢复的执行流程。 - 关键规则:
- 调用
async def函数并不会执行函数体,而是返回一个协程对象。 - 要让协程真正执行,必须通过
await或将其包装成任务丢给事件循环。 await只能用在async def函数内部,后面可以跟一个协程对象或任何可等待对象(如 Task、Future)。- 简单示例:
async def say_hello():
print("Hello")
await asyncio.sleep(1) # 模拟耗时 I/O,让出控制权
print("World")
# 下面直接调用只返回协程对象,不会打印任何内容
coro = say_hello() # <coroutine object say_hello at 0x...>
# 要真正执行,必须用 asyncio.run()
asyncio.run(say_hello())
任务(Task)
- 是什么:Task 是对协程对象的一种高级封装,它能让一个协程对象在事件循环中“后台”并发执行。你可以把它当成启动了一个异步的“轻量级线程”。
- 为什么需要 Task:单纯
await coro会当前协程会阻塞等待另一个协程执行完才继续。如果想让多个协程同时跑(比如同时下载三个网页),就需要把它们包装成 Task,让事件循环调度它们交替执行。 - 创建 Task 的方法:
asyncio.create_task(coro):最常用,把协程对象注册进当前运行的事件循环,立即返回一个 Task 对象。asyncio.ensure_future(coro):老式写法,功能类似。- 并发例子:
async def download(url):
# 模拟下载耗时
await asyncio.sleep(1)
print(f"下载完成: {url}")
async def main():
# 将两个协程包装成 Task,让它们并发运行
task1 = asyncio.create_task(download("url1"))
task2 = asyncio.create_task(download("url2"))
# 等待所有任务完成
await task1
await task2
asyncio.run(main())
# 输出:大约 1 秒后同时打印两行下载完成,而非先等 1 秒再等 1 秒
三者关系总结
- 事件循环 是调度中心,始终在运行。
- 协程对象 是可暂停的函数执行体,需要通过某些方式激活。
- Task 是协程对象的“并发包装”,让它们能在事件循环中被调度,真正实现并发。
理解了这个三角关系,就掌握了 asyncio 的基本工作原理:你用 async def 编写可能挂起的逻辑,用 asyncio.create_task 把这些逻辑注册进事件循环,事件循环则通过 await 处的让出机会,让多个任务交替执行,从而实现单线程下的高并发 I/O 处理。