Python 中直接 await 和 asyncio.create_task 有什么区别?
简化版
直接 await coro() 会等待这个协程完成,当前协程在这里暂停;asyncio.create_task(coro()) 会把协程包装成 Task 并立即交给事件循环调度,让它可以和当前协程并发执行。想顺序执行用 await,想启动后台并发任务用 create_task,但创建的 Task 要妥善等待或处理异常。
详细版
顺序执行:
async def main():
a = await fetch_a()
b = await fetch_b()
这里 fetch_b() 要等 fetch_a() 完成后才开始。
并发执行:
async def main():
task_a = asyncio.create_task(fetch_a())
task_b = asyncio.create_task(fetch_b())
a = await task_a
b = await task_b
两个任务会尽早被事件循环调度,整体耗时通常接近较慢的那个任务。
注意点:
create_task()需要运行中的事件循环;- Task 创建后就可能开始执行;
- 不要创建 Task 后完全不管,否则异常可能丢失或收尾不可控;
- 需要结构化并发时可以考虑
asyncio.TaskGroup。
面试回答重点:await 是等待一个 awaitable,create_task 是把协程安排成并发任务。
完整版教学
一、直接 await 更像顺序流程
看这段代码:
async def main():
user = await get_user()
orders = await get_orders(user.id)
这里订单依赖用户 ID,所以顺序是合理的。await get_user() 没完成前,后面的代码不会继续。
直接 await 的特点是控制流清楚,适合有依赖关系的步骤。
二、create_task 会把协程交给事件循环调度
如果两个请求互不依赖:
async def main():
profile_task = asyncio.create_task(get_profile())
message_task = asyncio.create_task(get_messages())
profile = await profile_task
messages = await message_task
create_task 把协程包装成 Task。Task 是事件循环可调度的单位,它会尽快开始执行。这样两个 I/O 等待可以重叠。
三、只创建协程对象不会自动并发
很多人以为这样就是并发:
coro1 = get_profile()
coro2 = get_messages()
profile = await coro1
messages = await coro2
这通常仍然是先等待 coro1,再等待 coro2。创建协程对象不等于调度执行。要让它们并发,应该创建 Task 或使用 gather。
四、不要让 Task 失控
这类代码有风险:
asyncio.create_task(send_log())
如果你不保存 Task,也不等待它:
- 任务异常可能只被事件循环记录;
- 程序退出时任务可能没执行完;
- 任务生命周期不可控;
- 测试中可能出现偶发失败。
更稳妥的做法是把任务纳入管理:
task = asyncio.create_task(send_log())
...
await task
或者使用 TaskGroup。
五、create_task 与后台任务
有些任务确实想后台执行,例如异步上报、预加载、并发请求。但后台不代表不用管。
你至少要考虑:
- 任务失败怎么记录;
- 程序关闭时怎么取消或等待;
- 是否需要超时;
- 是否会无限增长;
- 是否会访问已经关闭的资源。
异步系统里最可怕的不是“没有并发”,而是“任务满天飞,谁也不知道它们什么时候失败”。
六、常见误区与追问
| 写法 | 是否立即并发调度 | 适合场景 |
|---|---|---|
await coro() | 不会和后续代码并发,先等它完成 | 有明确顺序依赖 |
task = create_task(coro()) | 注册到事件循环,后续可并发推进 | 多个独立任务 |
只写 coro = coro_func() | 不运行 | 只是创建协程对象 |
await task | 等已有任务结果 | 收尾、拿返回值、传播异常 |
- 误区:调用 async 函数就会开始执行。 调用只创建协程对象,没人 await 或调度它时,函数体不会真正跑起来。
- 误区:create_task 后可以不管它。 失控后台任务可能吞掉异常、泄漏资源、在请求结束后继续访问无效上下文。
- 误区:await 一定会并发。 如果连续
await a(); await b(),两个操作是顺序完成,除非先创建任务再一起等待。 - 追问:什么时候直接 await 更好? 后一步依赖前一步结果,或只想表达顺序流程时,直接
await更清楚。 - 追问:什么时候 create_task 更好? 多个相互独立的 I/O 操作需要重叠等待时间时,先创建任务再统一等待。
- 追问:后台任务异常怎么处理? 应保留 task 引用、设置回调、集中 await,或放入结构化并发管理,避免异常无人观察。
记忆钩子:
await是“现在等它”,create_task是“先交给循环跑,稍后再管它”。
七、加强记忆
await 解决“我现在要等结果”,create_task 解决“我先把它安排出去和别人一起跑”。协程对象只是待执行的东西,Task 才是交给事件循环调度的任务。创建 Task 后要负责它的结果、异常和生命周期,不能随手丢掉。