asyncio 基础
约 1423 字大约 5 分钟
2026-05-10
asyncio 是 Python 标准库中的异步 IO 框架。它使用协程在单线程内并发执行大量 IO 任务。
- 协程是什么
- asyncio.run
- await
- 并发执行协程
- 1写一个 async def hello(),直接调用得到的是协程对象,不是返回值;用 asyncio.run(hello()) 才会真正执行。
- 2对比 await asyncio.sleep(1) 和 time.sleep(1):前者让出控制权,后者卡死整个事件循环。
- 3用 asyncio.gather(work('A'), work('B'), work('C')) 同时跑三个任务,总耗时约 1 秒而非 3 秒。
- 4故意在 async 函数里 time.sleep(3),体会“一个阻塞调用拖死所有协程”。
- 5调同步库怎么办:用 await asyncio.to_thread(blocking_func) 把它丢到线程池。
asyncio 是 Python 标准库中的异步 IO 框架。它使用协程在单线程内并发执行大量 IO 任务。
协程是什么
协程是一种可以暂停和恢复的函数。普通函数一旦开始执行,就会一直运行到返回;协程可以在 await 处暂停,把控制权交还给事件循环。
async def hello():
print('hello')调用协程函数不会立即执行,而是返回一个协程对象。
coro = hello()要真正运行协程,需要事件循环。
asyncio.run
import asyncio
async def main():
print('hello asyncio')
asyncio.run(main())asyncio.run() 会创建事件循环,运行主协程,并在结束后关闭事件循环。
await
await 用来等待一个可等待对象,同时把控制权交给事件循环。
import asyncio
async def download():
print('开始下载')
await asyncio.sleep(1)
print('下载完成')
asyncio.run(download())asyncio.sleep() 是异步睡眠,不会阻塞整个线程。
并发执行协程
如果直接 await 两个协程,它们仍然是顺序执行:
await download('a')
await download('b')要并发执行,需要创建 Task。
import asyncio
async def download(name):
print(f'开始 {name}')
await asyncio.sleep(1)
print(f'完成 {name}')
async def main():
task1 = asyncio.create_task(download('a'))
task2 = asyncio.create_task(download('b'))
await task1
await task2
asyncio.run(main())两个任务会在 sleep 等待期间交替执行。
事件循环
事件循环负责调度协程任务。它会不断检查:
- 哪些任务可以继续执行
- 哪些任务正在等待 IO
- 哪些定时器到期了
- 哪些回调需要执行
你可以把事件循环理解为一个调度员。协程遇到 await 时主动让出控制权,事件循环再调度其他协程执行。
asyncio 不是多线程
默认情况下,asyncio 在单线程中运行。它的并发来自协程主动让出控制权,而不是操作系统线程抢占。
因此:
- 协程切换成本低
- 适合高并发 IO
- 不适合直接执行耗时 CPU 计算
- 如果协程中写了阻塞代码,会卡住整个事件循环
反例:
import time
async def bad():
time.sleep(3) # 阻塞整个事件循环应该改成:
await asyncio.sleep(3)适用场景
asyncio 适合:
- 高并发网络请求
- WebSocket
- 异步 Web 服务
- 异步数据库访问
- 异步爬虫
不适合:
- 大量 CPU 计算
- 主要依赖同步阻塞库的项目
- 很简单的一次性脚本
asyncio 适合解决什么问题
asyncio 适合大量 IO 等待型任务,例如:
- 同时请求很多 HTTP 接口;
- 同时维护很多 WebSocket 连接;
- 写异步爬虫;
- 和异步数据库、消息队列客户端配合使用。
它不适合直接加速普通 CPU 计算。一个协程里如果一直做复杂计算、不 await,事件循环就没有机会切换到别的协程。
await 可以理解成“让出控制权”
看到:
await asyncio.sleep(1)可以粗略理解为:当前协程说“我这里要等一会儿,事件循环你先去跑别的任务吧”。
这和普通的:
time.sleep(1)区别很大。time.sleep() 会阻塞当前线程,事件循环也会被卡住;asyncio.sleep() 会把等待时间让出来。
一个对比例子
import asyncio
import time
async def good_task(name):
print('开始', name)
await asyncio.sleep(1)
print('结束', name)
async def main():
started = time.perf_counter()
await asyncio.gather(
good_task('A'),
good_task('B'),
good_task('C'),
)
print('耗时', time.perf_counter() - started)
asyncio.run(main())三个任务都会等待 1 秒,但总耗时大约也是 1 秒多,而不是 3 秒多。原因是等待期间事件循环可以切换到其他协程。
async 函数不会自己运行
定义 async 函数只是得到一个协程函数:
async def hello():
print('hello')调用它:
coro = hello()这时里面的代码还没有真正执行。你需要:
asyncio.run(hello())或者在另一个协程里:
await hello()这是初学 asyncio 最常见的卡点之一。
不要在 async 代码里随便调用阻塞函数
下面这种写法会卡住事件循环:
import time
async def bad_task():
time.sleep(5) # 不推荐应该换成异步版本:
import asyncio
async def good_task():
await asyncio.sleep(5)同理,异步程序里要优先使用异步 HTTP 客户端、异步数据库客户端。如果只能使用同步阻塞库,就要考虑放到线程池里执行。
总结
asyncio 的核心是事件循环、协程、await 和 Task。它不是让代码自动变快,而是在 IO 等待时让其他协程继续执行。理解“协程必须主动让出控制权”,是学习 asyncio 的关键。
- asyncio 适合大量 IO 等待型任务,不会自动加速 CPU 计算。
- `await` 可以理解为在等待时把控制权交还给事件循环。
- async 函数调用后不会立刻运行,需要 `asyncio.run()`、`await` 或创建任务来驱动。
版权所有
版权归属:Shuo Liu
