前两天在做数据分析项目时,需要同时爬取十几个数据源,结果同步请求跑完一轮要将近两分钟……优化完之后只用了不到8秒。今天就来聊聊这个让Python性能起飞的关键技术:异步编程。很多同学一听到"异步"就觉得是高级话题,其实它的核心思想和餐厅点菜一模一样,你不需要站在厨房门口等厨子炒完第一道菜再点第二道,点完坐下玩手机,菜好了服务员自然会端上来。
Python的异步编程,就是把"等菜"的时间省下来干别的。
事件循环(Event Loop):异步世界的"餐厅老板"
异步编程最核心的概念是事件循环。
你可以把它想象成一个不停地接单、派活、收结果的餐厅老板:
import asyncioasync def main(): print("开始做饭...") await asyncio.sleep(2) # 模拟耗时操作 print("饭好了!")asyncio.run(main()) # 老板开始干活asyncio.run() 创建了一个事件循环,它驱动着整个异步程序的执行。
当你 await 某个任务时,事件循环不会傻等,它会立刻切去处理其他任务,等这个任务完成了再回来处理结果。
这跟多线程有本质区别。多线程是"多雇几个厨子",而异步是"一个厨子同时盯好几个锅"。
多线程有线程切换开销、GIL锁竞争、共享状态的各种坑;异步是单线程内协作式调度,天然避免了这些并发问题。
async/await 不是装饰,而是协议
很多初学者以为 async def 只是给函数贴个标签,实际上它在底层定义了一套完整的协议。
async def 定义的函数返回的是一个协程对象,不是普通的函数返回值。调用 async def 函数() 并不会执行函数体,而是返回一个coroutine,必须交给事件循环驱动才能运行:
async def fetch(url): return f"来自 {url} 的数据"coro = fetch("https://api.example.com")print(type(coro)) # <class 'coroutine'># coro 此时还没有被执行!更深一层,await 后面跟的必须是可等待对象,也就是实现了 __await__ 协议的对象。
Python内置了三种可等待类型:Coroutine、Task 和 Future。
• Coroutine:最基础,async def 的返回值
• Task:把协程包装成任务交给事件循环调度
• Future:低层级的异步结果占位符,Task 就是 Future 的子类
理解这个层级关系很关键:当你写 await asyncio.sleep(1) 时,实际上 sleep 返回一个 Future,事件循环把这个 Future 挂起来,1秒后把它标记为完成,再唤醒等你拿结果。
asyncio 的并发大招:gather 与 create_task
异步真正的威力在于并发,来看一个真实场景:
import asyncioimport timeasync def download(item): await asyncio.sleep(1) # 模拟IO操作 return f"{item} 下载完成"async def main(): start = time.time() # 并发执行3个下载任务 results = await asyncio.gather( download("file1"), download("file2"), download("file3") ) print(f"耗时: {time.time() - start:.2f}秒") # ~1秒,不是3秒! print(results)asyncio.gather() 同时启动多个协程,等所有任务完成后统一返回结果。如果用同步方式顺序执行,需要3秒;异步并发只需1秒。
但这里有个常见的坑:gather 里的任务是无差别并发的。
如果其中一个抛出异常,默认情况下 gather 不会中断其他任务,而是在返回后统一抛出。
你要是想"一个挂全挂",可以调整异常处理策略。
另一个更灵活的选择是 asyncio.create_task(),它把协程包装成 Task 并立即提交给事件循环,返回一个可以单独跟踪的 Task 对象:
async def main(): task1 = asyncio.create_task(download("file1")) task2 = asyncio.create_task(download("file2")) # 中间可以做其他事 print("任务已提交,继续做别的...") result1 = await task1 result2 = await task2create_task 给了你更精细的控制,可以单独取消某个任务、查询某个任务的状态,适合任务之间有依赖关系或需要动态管理并发数的场景。
全栈实战:从FastAPI到数据库连接池
学了这么多概念,真正落地才是最爽的。
以 FastAPI 为例,它本身就是基于 Starlette 的异步框架,天然支持 async def 路由:
from fastapi import FastAPIimport httpxapp = FastAPI()@app.get("/aggregate")async def aggregate_data(): async with httpx.AsyncClient() as client: users, orders, products = await asyncio.gather( client.get("http://user-service/users"), client.get("http://order-service/orders"), client.get("http://product-service/products"), ) return {"users": users.json(), "orders": orders.json(), "products": products.json()}数据库也一样。不要再用同步的 pymysql 了,换成异步驱动asyncpg 配 PostgreSQL、aiomysql 配 MySQL、motor 配 MongoDB,性能直接翻几倍:
import asyncpgasync def fetch_users(): conn = await asyncpg.connect("postgresql://...") rows = await conn.fetch("SELECT * FROM users LIMIT 100") await conn.close() return rows异步数据库连接池更是好用:连接复用、自动管理、故障重连,这些都是事件循环帮你默默搞定的。
避坑指南:那些年我踩过的异步坑
1. 别在 async 函数里用同步阻塞库:requests.get() 会阻塞整个事件循环,换成 httpx.AsyncClient 或 aiohttp
2. 注意 CPU 密集型任务的异味:计算密集的活不适合放 async 里,会卡住整个循环,这时候该用 run_in_executor 丢到线程池
3. 别忘了 await:写成 download("file") 而不加 await,只会拿到一个 coroutine 对象,并不会真正执行
结语
异步编程是 Python 全栈开发的一道分水岭。掌握它,你才能真正写出高性能、高并发的后端服务。从 eventloop 的底层原理到 asyncio 的实战技巧,希望这篇文章能帮你打通任督二脉。
下次再遇到"一堆IO操作卡半天"的场景,记住:异步一下,世界就快了。
今天的内容就到这里,感谢你看到最后
琴萧双手奉上一份Python大礼包(总共155G),涵盖爬虫、web开发、数据分析、机器学习、人工智能等等的学习资料
点击下方卡片,发送20260626 即可领取