Python 异步编程(asyncio)是处理高并发 IO 场景的利器。当你的程序大部分时间在等待网络、数据库或文件 IO 时,asyncio 用单线程事件循环就能轻松扛住成千上万的并发连接,而不必为每个请求开一个线程。本文从协程、事件循环讲到并发原语与真实落地,带你搞清楚异步 IO 到底能做什么、不能做什么,以及怎样才能写出既快又不会踩坑的代码。
一、为什么需要异步:同步阻塞的代价
假设你有一个接口需要串行请求 3 个外部 API,每个平均耗时 300 毫秒。同步写法下,总耗时就是 300 + 300 + 300 = 900 毫秒,因为线程在等待第一个响应时什么都做不了。在 Web 服务里,这种阻塞会直接吃掉连接池和线程资源,并发一高就排队甚至超时。
asyncio 的核心思路是:当一个协程遇到 IO 等待(网络、磁盘、sleep),它主动把控制权交还事件循环,让循环去调度其他就绪的协程。于是单线程也能在等待期间”同时”推进成百上千个任务。它解决的是 IO 密集型并发,而非 CPU 密集型计算。
二、协程与 async/await 基础
在 Python 中,用 async def 定义的都是协程函数(coroutine function),调用它不会立刻执行,而是返回一个协程对象。真正的执行需要事件循环来驱动。
import asyncio
async def fetch(name, delay):
print(f"{name} 开始,等待 {delay}s")
await asyncio.sleep(delay) # 模拟 IO 等待,主动让出控制权
print(f"{name} 完成")
return name
# asyncio.run 会创建事件循环并运行到协程结束
async def main():
result = await fetch("任务A", 1)
print("拿到结果:", result)
if __name__ == "__main__":
asyncio.run(main())
关键点是 await:它只能在 async def 内部使用,表示”在这里可能要等,先把执行权交出去”。没有 await 的协程不会真正让出,也就谈不上并发。
三、并发执行:gather、wait 与任务
真正的并发来自同时调度多个协程。最常用的是 asyncio.gather,它把多个协程/任务并发跑完、按输入顺序收集结果。
async def main():
# 三个任务"同时"开始,总耗时约等于最慢的那个(约 1s),而非 3s
results = await asyncio.gather(
fetch("A", 1),
fetch("B", 1),
fetch("C", 1),
)
print(results) # ['A', 'B', 'C']
asyncio.run(main())
如果你希望”谁先完成先处理谁”,可以用 asyncio.as_completed 遍历已完成的任务;需要给单个任务设超时,则用 asyncio.wait_for。三者的取舍如下:
| 原语 | 特点 | 适用场景 |
| gather | 并发收集全部结果,顺序与输入一致;任一异常会冒泡 | 要一次性拿齐所有结果 |
| as_completed | 按完成顺序逐个产出,先到先得 | 流式处理、先完成先落库 |
| wait_for | 给单个可等待对象加超时上限 | 防止某个慢调用拖垮整体 |
四、异步上下文管理器与迭代器
除了函数,asyncio 还支持 async with(异步上下文管理器)和 async for(异步迭代器),这让资源管理与流式读取也变得”可等待”。
class AsyncResource:
async def __aenter__(self):
print("获取连接")
return self
async def __aexit__(self, *exc):
print("释放连接")
async def main():
async with AsyncResource() as r:
await asyncio.sleep(0.1)
print("使用资源中")
五、实战:用 aiohttp 并发抓取网页
最典型的高并发 IO 场景就是批量 HTTP 请求。下面用 aiohttp 并发抓取多个 URL,单连接也能轻松并发几十个请求。
import asyncio, aiohttp
async def get(session, url):
async with session.get(url) as resp:
return url, resp.status, len(await resp.text())
async def crawl(urls):
# 限制并发,避免把目标站打挂
connector = aiohttp.TCPConnector(limit=20)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = [get(session, u) for u in urls]
for coro in asyncio.as_completed(tasks):
url, status, size = await coro
print(f"{url} -> {status}, {size} bytes")
urls = ["https://example.com"] * 10
asyncio.run(crawl(urls))
注意 TCPConnector(limit=20) 这一步:无脑并发成百上千请求既会压垮对方,也容易触发自己的文件描述符上限。生产环境一定要做并发限流,这和我们在 SSH 连接故障排查里强调的”连接数要设上限”是同一个道理。
六、实战:异步操作数据库
关系型数据库驱动也有异步版本,例如 PostgreSQL 的 asyncpg、MySQL 的 aiomysql。它们让 Web 接口在等待查询返回时也能处理别的请求。
import asyncio, asyncpg
async def query():
conn = await asyncpg.connect(
user="app", password="pwd",
database="demo", host="127.0.0.1",
)
rows = await conn.fetch("SELECT id, name FROM users WHERE age > $1", 18)
await conn.close()
return rows
async def main():
# 并发查 3 次,总耗时接近单次而非 3 倍
results = await asyncio.gather(query(), query(), query())
print(sum(len(r) for r in results))
asyncio.run(main())
七、同步与异步的边界:什么时候该用、不该用
异步不是银弹。它最大的收益在 IO 密集,对 CPU 密集任务几乎无能为力——因为事件循环本身仍在单线程里跑,一段耗时的纯计算会阻塞整个循环。
| 任务类型 | 是否适合 asyncio | 原因 / 替代方案 |
| HTTP / 数据库 / 文件 IO | 非常适合 | 大部分时间在等待,协程让出后可调度其他任务 |
| 密集计算(压缩、加密、图像处理) | 不适合 | 用 multiprocessing 多进程,或 run_in_executor 卸载 |
| 调用同步阻塞库 | 谨慎 | 用 loop.run_in_executor 丢到线程池,避免卡住循环 |
当你不得不调用一个同步阻塞函数(比如某个还没支持异步的老库),可以用 run_in_executor 把它丢到默认线程池,从而不阻塞事件循环:
import asyncio, time
def block_cpu(n):
return sum(i * i for i in range(n))
async def main():
loop = asyncio.get_running_loop()
# 把 CPU 密集任务交给线程池,事件循环继续转
result = await loop.run_in_executor(None, block_cpu, 10_000_000)
print("计算结果:", result)
asyncio.run(main())
八、避坑清单
- 别在协程里调阻塞代码:一个
time.sleep或同步 requests 调用会卡住整个事件循环,必须换成await asyncio.sleep或异步库。 - 不要为每个请求新建事件循环:在同一个程序里尽量复用
asyncio.run或一个长期运行的 loop,循环开销虽小但新建会带来状态割裂。 - 小心”忘记 await”:漏写 await 时代码不会报错,但协程根本不会执行,是最难排查的静默 Bug。
- 共享状态要加锁:协程切换点可能发生在任意 await 处,跨协程共享变量请用
asyncio.Lock而非假设”单线程就安全”。
九、和 Node.js 事件循环的关系
如果你熟悉前端或 Node.js,会发现 asyncio 的思路和 Node.js 事件循环一脉相承:都是单线程 + 事件循环 + 非阻塞 IO。区别在于 Python 用 async/await 语法把”可等待点”写得一清二楚,而 Node 早期靠回调、后来才补上 Promise 与 async 语法。理解其中一个,另一个也就触类旁通了。
十、落地清单
- IO 密集型服务(爬虫、网关聚合、批量调用)优先评估 asyncio;
- 并发请求务必加限流(Connector limit / Semaphore),别无脑并发;
- 数据库、HTTP 一律用对应的异步驱动(asyncpg、aiohttp、httpx);
- 遇到同步阻塞库,用
run_in_executor卸载到线程池; - CPU 密集任务走多进程,别指望事件循环能救场;
- 上线前用压测量化收益,确认 P99 真的下降。
Python 异步编程的门槛不在语法,而在”什么时候用、怎么限流、如何不被阻塞调用坑到”。把上面这几条吃透,你就能用 asyncio 稳稳写出高并发的 IO 应用,而不是写出一堆更隐蔽的并发 Bug。




