别再误用 asyncio 了:从串行到并发的正确姿势

2025博客之星年度评选已开启 10w+人浏览 3.3k人参与

一、串行执行的陷阱

问题代码示例

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 秒!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值