在Python的asyncio编程模型里,协程函数调用后返回的是一个协程对象,它本身并不会被执行,更不会被事件循环调度。只有通过asyncio.create_task()把协程包装成Task,才能真正进入事件循环的就绪队列,和其他任务并发运行。这种方式是现代asyncio推荐的显式任务创建手段,替代了旧版本中直接操作事件循环对象的写法。

asyncio.create_task()的基本用法
asyncio.create_task()接收一个协程对象,并将其安排到当前线程正在运行的事件循环中去执行,同时立即返回一个Task实例。这个Task是Future的子类,既可以被await,也能查询状态或取消。下面的例子展示了如何并发启动两个下载任务:
import asyncio
async def download(name, seconds):
print(f"开始下载 {name}")
await asyncio.sleep(seconds)
print(f"{name} 下载完成")
return name
async def main():
# 把协程包装为任务,立即加入事件循环调度
task1 = asyncio.create_task(download("文件A", 2))
task2 = asyncio.create_task(download("文件B", 1))
# 等待两个任务结束
result = await asyncio.gather(task1, task2)
print(result)
asyncio.run(main())
在上面代码中,task1和task2在创建后就已经处于调度状态,事件循环会在合适的时机运行它们。由于download("文件B", 1)只需1秒,它会比文件A先打印完成信息,这说明两个协程确实在并发执行,而不是顺序阻塞。
需要注意,create_task()依赖一个正在运行的事件循环。也就是说,它只能在由asyncio.run()或loop.run_until_complete()驱动的主协程内部调用。如果在同步函数或没有活跃循环的上下文中调用,解释器会报RuntimeError: no running event loop。这一点和直接定义协程对象有本质区别。
与直接await协程的区别
很多初学者会混淆await coro()和asyncio.create_task(coro())。前者是串行等待:必须等这个协程彻底跑完,控制权才交还;后者是并发登记:任务被提交后,主协程可以继续做别的事,直到显式await该任务。下面的对比代码能清楚看出差异:
import asyncio
async def step(name, n):
for i in range(n):
print(name, i)
await asyncio.sleep(0.5)
async def serial():
await step("串行A", 3)
await step("串行B", 2)
async def concurrent():
t_a = asyncio.create_task(step("并发A", 3))
t_b = asyncio.create_task(step("并发B", 2))
await t_a
await t_b
asyncio.run(concurrent())
在serial()里,A的三次打印全部结束才开始B;而在concurrent()里,A和B的打印会交替出现,因为两个任务都被交给了事件循环。由此可见,如果不使用create_task(),即使写了async函数,也享受不到异步并发带来的吞吐提升。
此外,Task对象还提供了诸如cancel()、done()、result()等接口。例如某个任务超时后,可以调用task.cancel()请求中止,事件循环会在下一个await点注入CancelledError。这种细粒度控制是单纯await协程做不到的。
任务调度时机与常见误区
一个常见误解是认为create_task()会立刻执行协程里的代码。实际上,它只是把任务放进循环的就绪队列,真正的第一步要等当前协程让出控制权(遇到await)后才可能发生。看下面这个例子:
import asyncio
async def say(msg):
print(msg)
async def main():
t = asyncio.create_task(say("任务已登记"))
print("主协程还在跑")
await t
asyncio.run(main())
输出顺序一定是先打印“主协程还在跑”,再打印“任务已登记”。因为create_task()返回后,main()还没遇到await,所以任务虽然已登记却没机会运行。只有await t时,main让出控制,say才开始执行。理解这个调度节奏,对排查“为什么任务没按时启动”类问题非常关键。
另一个误区是在循环外缓存协程对象再跨循环create_task。由于Task绑定到创建时的那个事件循环,若在程序启动早期(无循环)生成协程,之后又在不同循环里create_task,就会触发循环不匹配错误。正确做法是每次都在目标循环上下文内调用create_task,或改用asyncio.ensure_future()并显式传入loop参数(不过ensure_future在3.10+也建议用create_task替代)。
取消任务与异常处理
Task支持主动取消,但取消不是瞬间杀死线程,而是通过向协程抛入CancelledError来协商退出。协程可以捕获该异常做清理,也可以重新抛出以真正终止。示例:
import asyncio
async def worker():
try:
while True:
print("工作中")
await asyncio.sleep(0.5)
except asyncio.CancelledError:
print("收到取消请求,清理资源")
raise
async def main():
t = asyncio.create_task(worker())
await asyncio.sleep(1.5)
t.cancel()
try:
await t
except asyncio.CancelledError:
print("主协程确认任务已取消")
asyncio.run(main())
这里worker捕获CancelledError后打印清理信息并重新raise,使Task状态变为cancelled。主协程在await t时也会收到该异常,从而知道任务结束方式。如果协程内部忽略了CancelledError且不重新抛出,任务会标记为正常完成,这可能掩盖真实的取消语义。
对于一组任务,可以使用asyncio.gather()配合return_exceptions=True来统一收集结果或异常,避免单个任务失败导致整个等待链崩溃。结合create_task做批量并发再集中await,是asyncio服务端程序的标准写法。
小结与最佳实践
使用asyncio.create_task()的核心价值在于把协程显式转化为可由事件循环调度的任务,从而获得并发能力。实践中应做到:只在运行中的循环内创建任务;理解create_task返回后不会立即执行;用Task的cancel机制做优雅退出;避免跨循环复用协程对象。掌握这些要点,就能写出既高效又不易出错的异步Python代码。
当业务逻辑需要从多个数据源并行获取结果时,先create_task登记所有协程,再用gather等待,是最清晰的范式。相比层层回调或手动管理Future,这种方式更符合Python的线性书写直觉,也更容易维护。
asynciocreate_task协程任务修改时间:2026-08-11 18:45:42