一、串行执行的陷阱
问题代码示例
import asyncio
import time
async def test(info):
await asyncio.sleep(1)
return f"这是任务 {info}"
async def main():
t1 = time.time()
results = []
for i in range(3):
res = await test(f"{i}")
results.append(res)
t2 = time.time()
print(f"返回值列表: {results}")
print(f"总耗时: {t2-t1}")
if __name__ == "__main__":
asyncio.run(main())
问题分析
这里虽然使用了 asyncio,但实际上是在串行执行任务。await test() 会阻塞 main 协程,直到当前任务完成才会进入下一次循环,这导致 3 次睡眠总共耗时约 3 秒,完全失去了异步并发的优势。
为什么执行到 await test(f"{i}") 时不能立即切换到下一个任务?
因为这里用了 await,就是在告诉程序:“我要等这个任务做完才能继续”。虽然 test 函数里遇到 await asyncio.sleep(1) 会把控制权交还给事件循环,但此时事件循环一看——main 函数还在等着第一个 test 完成呢,并且此时没有其他任务可以干,因为只创建了一个任务,就是当前正在执行的这个 test(f"{i}")。
事件循环(Event Loop)这时候在做什么?
它就像一个调度员,检查有没有其他可以做的事情。但因为 main 函数在await test(f"{i}"),没有提前创建其他任务,调度员也只能干等着。等 test 睡完 1 秒醒来后,main 才进入下一轮循环,再创建下一个任务。
因此,每执行一次 test 协程,事件循环都会被迫等待 1 秒钟,导致总耗时为 3 秒钟。虽然用了 asyncio,但因为任务是一个接一个等着完成的,完全变成了"排队执行",没有利用到异步可以"同时进行多个任务"的能力。
二、正确的异步并发写法
要实现真正的异步并发执行,以下是几种更优雅且高效的写法:
方法一:使用 asyncio.gather() + create_task()
可以使用 asyncio.create_task() 来创建任务,并将它们调度到事件循环中。这样,所有任务可以同时开始执行,而不是一个接一个地等待完成。
如果你需要动态生成大量任务,可以先创建任务列表,再统一等待。在这个修改后的代码中,main 协程创建了多个任务并将它们添加到一个任务列表中。然后,使用 asyncio.gather() 来并发地等待所有任务完成。这样,所有任务会同时开始执行,总耗时大约为 1 秒钟,而不是 3 秒钟,从而充分利用了 asyncio 的异步并发特性。
如果你有一组任务并且想同时开始运行它们,然后等待所有任务完成,这是最标准的方法。这里会按照传入任务的顺序依次返回结果。
import asyncio
import time
async def test(info):
await asyncio.sleep(1)
return f"这是任务 {info}"
async def main():
t1 = time.time()
tasks = []
for i in range(3):
tasks.append(asyncio.create_task(test(f"{i}")))
results = await asyncio.gather(*tasks)
t2 = time.time()
print(f"返回值列表: {results}")
print(f"总耗时: {t2-t1}")
if __name__ == "__main__":
asyncio.run(main())
更简洁的写法
其实也可以直接将协程传递给 asyncio.gather(),省去显式创建任务对象的步骤:
import asyncio
import time
async def test(info):
await asyncio.sleep(1)
return f"这是任务 {info}"
async def main():
t1 = time.time()
tasks = [test(f"{i}") for i in range(3)]
results = await asyncio.gather(*tasks)
t2 = time.time()
print(f"返回值列表: {results}")
print(f"总耗时: {t2-t1}")
if __name__ == "__main__":
asyncio.run(main())
方法二:使用 as_completed()
可以使用 as_completed() 来处理任务,这样可以在每个任务完成时立即返回结果,而不必等待所有任务都完成。这里会按照任务完成的顺序依次返回结果,可能与传入顺序不同。
import asyncio
import time
async def test(info):
await asyncio.sleep(1)
return f"这是任务 {info}"
async def main():
t1 = time.time()
results = []
tasks = [test(f"{i}") for i in range(3)]
for coro in asyncio.as_completed(tasks):
res = await coro
results.append(res)
t2 = time.time()
print(f"返回值列表: {results}")
print(f"总耗时: {t2-t1}")
if __name__ == "__main__":
asyncio.run(main())
在这个示例中,main 协程创建了多个任务并使用 asyncio.as_completed() 来获取一个迭代器,该迭代器会在每个任务完成时返回相应的协程。这样,任务可以并发执行,并且每个任务完成后会立即处理结果。总耗时仍然约为 1 秒钟。
方法三:使用 asyncio.TaskGroup(Python 3.11+)
对于 Python 3.11 及其更高版本,asyncio.TaskGroup 提供了一种比传统的 asyncio.gather 更安全、更结构化的方式来管理并发任务。这里会按照传入任务的顺序依次返回结果。
import asyncio
import time
async def test(info):
await asyncio.sleep(1)
return f"这是任务 {info}"
async def main():
t1 = time.time()
results = []
async with asyncio.TaskGroup() as tg: # 注意这里使用的是async with
tasks = [tg.create_task(test(f"{i}")) for i in range(3)]
for task in tasks:
results.append(task.result())
t2 = time.time()
print(f"返回值列表: {results}")
print(f"总耗时: {t2-t1}")
if __name__ == "__main__":
asyncio.run(main())
三、asyncio.TaskGroup 的核心优势
与旧的 asyncio.gather 相比,TaskGroup 有几个显著优点:
1. 结构化并发(Structured Concurrency)
它强制要求任务在一个明确的作用域(async with)内开启和结束。你不需要手动追踪任务列表。
2. 更强的异常管理
一旦 TaskGroup 中的任何一个任务抛出异常,组内其他尚未完成的任务都会被自动取消。它抛出的是 ExceptionGroup(也是 Python 3.11 新特性),这意味着如果多个任务同时报错,你可以捕获并处理所有错误,而不仅仅是第一个。
异常处理示例
在 Python 3.11+ 中,当 TaskGroup 抛出错误时,e 实际上是一个 ExceptionGroup 对象。如果你想更精确地捕获 ValueError,建议使用新的 except* 语法:
import asyncio
import time
async def test(info):
await asyncio.sleep(1)
if info == "1":
raise ValueError(f"任务 {info} 失败了!")
return f"这是任务 {info}"
async def main():
t1 = time.time()
results = []
try:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(test(f"{i}")) for i in range(3)]
except* ValueError as eg: # 注意:使用 except* 捕获 ExceptionGroup
print(f"捕获到 {len(eg.exceptions)} 个 ValueError:")
for exc in eg.exceptions:
print(f" - {exc}")
except* Exception as eg: # 捕获其他所有异常
print(f"捕获到其他异常:")
for exc in eg.exceptions:
print(f" - {type(exc).__name__}: {exc}")
for i, task in enumerate(tasks):
try:
result = task.result()
results.append(result)
print(f"任务 {i}: {result}")
except Exception as e:
results.append(None)
print(f"任务 {i}: 失败 - {e}")
t2 = time.time()
print(f"\n返回值列表: {results}")
print(f"总耗时: {t2 - t1:.2f}")
if __name__ == "__main__":
asyncio.run(main())
总结
- 串行执行:在循环中直接
await会导致任务串行执行,无法发挥异步优势 - 并发方案一:
asyncio.gather()- 适合需要按顺序获取所有结果的场景 - 并发方案二:
as_completed()- 适合需要立即处理完成任务的场景 - 并发方案三:
TaskGroup(Python 3.11+)- 提供结构化并发和更强的异常管理
选择合适的方法可以显著提升程序性能,将耗时从 3 秒缩短到 1 秒!

被折叠的 条评论
为什么被折叠?



