Python asyncio 能让我的程序不再傻等吗?

摘要:1. 痛点场景:你是在“单线程”思考吗? 想象你正在开发一个爬虫程序,需要下载 100 张高清图片。 如果你用传统的 requests 库,代码逻辑通常是这样的: 发起请求 A -> 等待网络响应(500ms) -&a
1. 痛点场景:你是在“单线程”思考吗? 想象你正在开发一个爬虫程序,需要下载 100 张高清图片。 如果你用传统的 requests 库,代码逻辑通常是这样的: 发起请求 A -> 等待网络响应(500ms) -> 保存图片 A。 发起请求 B -> 等待网络响应(500ms) -> 保存图片 B。 ...以此类推。 问题出在哪里? 在那 500ms 的网络等待时间里,你的 CPU 实际上在摸鱼!它明明可以处理剩下的 99 个请求,却非要死等这一个响应回来。这种模式叫“同步阻塞”,是导致程序运行缓慢的头号元凶。 解决方案: asyncio。它让 Python 学会了“分身术”,在等待 A 的时候,顺手把 B、C、D 全都发出去。
2. 概念拆解:米其林餐厅的秘密 生活化类比 为了理解 asyncio,我们把 CPU 比作 餐厅厨师。 同步阻塞(Synchronous):厨师把牛排丢进锅里,然后死死盯着锅,直到肉熟了才去切土豆。这时候,哪怕外面排了 10 个客人,厨师也什么都不干。 异步非阻塞(Asyncio):厨师把牛排丢进锅里,定个闹钟(注册事件),转身就去切土豆或准备酱汁。等闹钟响了,他再回来翻牛排。 在这个比喻中: 事件循环 (Event Loop):就是那个“闹钟管理器”。它负责监控哪些任务做好了,该切回哪一环。 协程 (Coroutine):就是“牛排煎制”或“切土豆”这些可以中途挂起、之后再继续的任务。 逻辑图解 提交任务:将多个协程扔进事件循环。 遇到 I/O:协程主动说:“我现在要等网络/硬盘,你先去忙别的吧(await)。” 切换执行:事件循环立刻拉起另一个准备好的协程。 回调恢复:I/O 返回后,事件循环在下一轮通知原协程继续。
3. 动手实战:从 Hello World 到并发请求 我们直接跳过无意义的 print("Hello"),来看一个模拟网络请求的最小可行性代码。 基础代码 Python import asyncio import time # 1. 定义一个协程函数(使用 async 关键字) async def fetch_data(id, delay): print(f"任务 {id}: 正在发起请求,预计耗时 {delay} 秒...") # 2. 使用 await 挂起当前任务,模拟网络 I/O # 注意:必须 await 一个支持异步的对象,time.sleep 是同步的,不能用在这里 await asyncio.sleep(delay) print(f"任务 {id}: 数据返回成功!") return f"结果 {id}" async def main(): start_time = time.perf_counter() # 3. 创建任务并发执行 print("--- 任务开始 ---") # gather 会同时调度多个协程 results = await asyncio.gather( fetch_data(1, 3), fetch_data(2, 1), fetch_data(3, 2) ) end_time = time.perf_counter() print(f"--- 所有任务完成,总耗时: {end_time - start_time:.2f} 秒 ---") print(f"返回列表: {results}") # 4. 运行事件循环 if __name__ == "__main__": asyncio.run(main()) 代码解析 async def: 告诉 Python 这是一个协程,调用它不会立即执行,而是返回一个协程对象。 await: 这是“暂停键”。它告诉事件循环:“我要在这儿等一会儿,你先去处理别人,等好了再叫我。” asyncio.gather: 这是“集合指令”,它把多个协程打包,让事件循环同时启动它们。 结果分析: 虽然总等待时间是 $3+1+2=6$ 秒,但你会发现程序运行只需 3 秒左右。因为最长的那个任务还没做完时,短的任务已经利用空隙做完了。
阅读全文