一、什么是异步编程?
异步编程的核心目标是:主线程不等待。
在同步编程中,代码是顺序执行的。如果程序遇到 I/O 阻塞(比如网络请求、磁盘读取等),就必须等待它完成,整个流程就"卡"在那里。而异步编程允许我们在遇到耗时操作时"把任务挂起",继续执行其他代码,等任务完成后再"回来"处理。
就像你是餐厅服务员,采用同步方式要一桌一桌服务,而异步方式允许你同时处理多个订单,哪道菜先做好就先端给顾客。
在异步编程中,这种可以被挂起、稍后恢复执行的任务对象叫作:可等待对象(awaitable)。
可等待对象的神奇之处就在于它是"可以等待的":当执行任务时遇到耗时操作,该任务会自动"停下"(挂起),而事件循环(就像餐厅后台调度员)则继续调度其它可等待的任务。
你可以把它想象成城市交通系统:
- 车辆:可等待对象
- 红绿灯:事件循环调度
- 整个交通系统:主线程
比如南北方向的车遇到红灯就停下了,而东西方向是绿灯的车就可以继续通行。这种由事件循环控制的切换,就是异步编程中的核心调度机制。
因此,"主线程不等待",靠的是在适当时刻让可等待对象等待,以换取整个程序的不阻塞流畅运行。
二、为什么需要可等待对象(Awaitable)?
教科书上通常这么说:为了实现"不等待但又能拿到结果",Python 引入了"可等待对象"这个抽象。
听起来有点拗口,那什么叫"不等待但又能拿到结果"呢?
我们来举个例子------
你去买早餐,老板说:"包子还在蒸,你先拿个号码牌,好了我叫你。"
这个号码牌就是"可等待对象"。它不是包子本身,但承诺你之后可以拿到。
对应代码如下:
python
import asyncio
async def 蒸包子():
await asyncio.sleep(2) # 模拟蒸包子的耗时过程
return "热腾腾的包子"
# 调用异步函数,得到的是一个"承诺"
号码牌 = 蒸包子()
print(号码牌) # <coroutine object 蒸包子 at 0x...>
# 要拿到实际结果,需要等待兑现承诺
包子 = await 号码牌
所以你调用异步函数时,并不会直接得到"包子"(结果),而是一个承诺------等会儿给你包子。这就是"不等待但又能拿到结果"的含义。
你拿着"承诺",可以去做别的事;等事件循环调度你了(叫你名字了),你再回来拿包子。
再回到交通例子:你创建了一辆车(调用协程函数,创建可等待对象),这辆车就必须遵守交通规则(必须 await 过)。你不能直接开(不能直接 print 结果),要等交通调度(事件循环)让你开,才能执行。
三、事件循环与任务调度
什么时候兑现承诺?什么时候包子才能真正到手?
这就需要"事件循环"出场了。事件循环(event loop)是异步编程的大脑,它负责调度所有可等待对象的执行顺序与时机。
我们继续用红绿灯类比:
- 你造了一辆车(创建可等待对象)
- 红绿灯控制什么时候你能走(事件循环调度任务)
只有当红灯变绿灯,车才可以启动。这就好比你 await
一个任务,它才会真正去执行、并返回结果。
事件循环的几个常见驱动方式如下:
csharp
await some_task # 在异步函数中使用
loop.run_until_complete(task) # 在同步环境中使用
asyncio.run(main()) # 推荐的封装方式
loop.run_forever() # 一直运行直到手动中止
注意:事件循环默认是"懒"的,不启动它就不会调度任务。
四、深入理解 asyncio.as_completed
这是 Python 异步编程中一个常见的并发处理工具,核心特点是:
🌱 它是规则的承诺制定者,而不是执行者。
示例:
ini
for cor in asyncio.as_completed(task_list):
result = await cor
print(result)
这一行:
scss
asyncio.as_completed(task_list)
它做了什么?
- 将每个协程包装成 Task 并注册进事件循环
- 返回一个可迭代对象,但它本身不执行任务
真正执行任务的是你后面的 await cor
:它触发事件循环去跑这个任务,并且按谁先完成谁先返回的规则来调度。
总结:
as_completed()
是承诺制定者,await
是兑现者。
五、不要混淆这些"规则制定者"
很多人以为下面这些函数会立即执行任务,其实它们只是返回了承诺,并没有调度执行:
scss
asyncio.gather()
asyncio.wait()
asyncio.as_completed()
只有你使用 await
(或其他方式启动事件循环)时,它们才会执行。
六、异步编程的哲学与跨语言对比
现代编程语言中的异步模型有一个共同的哲学:Future / Promise 模式
语言 | 术语 | 意义 | 说明 |
---|---|---|---|
Python | Awaitable / Task | 未来可以等待的任务 | |
JavaScript | Promise | 承诺未来的值 | |
C# | Task | 异步任务 | |
Rust | Future | 等待未来的值 |
这个模型都遵循两个阶段:
- 承诺制定阶段:创建任务,但不执行
- 调度兑现阶段:交给调度器运行,获取结果
七、总结一句话:
异步编程的核心是不等待,为了实现这个目标,我们引入了"可等待对象"。
这个看似矛盾的说法,其实就是异步编程的哲学精髓。
主线程不等待,是因为我们让任务对象有选择地等待。
再次类比城市交通: 为了让整个交通系统流畅(主线程不等待),就要让特定方向的车辆(任务)在红灯时停下(等待),绿灯时再通行(继续执行)。
🎓 延伸思考
- 同步编程关注的是"顺序点" ------ 谁写在前谁执行
- 异步编程关注的是"时间点" ------ 谁先完成谁先返回
真正优秀的系统,必须通过分离承诺定义与执行调度,实现最大自由、最小耦合。