Python asyncio并发执行多个任务:create_task与gather用法示例

洛溪
洛溪
发布时间:2026-11-14| 5人浏览过
原创

深入解析Python asyncio并发执行多个任务的核心机制,详解asyncio.create_task与asyncio.gather的区别、用法示例、执行流程与常见避坑指南。

在编写网络请求、批量数据抓取或高吞吐量 I/O 服务时,串行等待每一个网络响应会让程序耗时随着请求量线性倍增。Python 提供的 asyncio 标准库通过单线程事件循环机制,允许我们在等待 I/O 阻塞的间隙切换执行其他任务,从而在极低资源开销下达成高效并发。实现多任务并发执行最基础也最常用的两个核心工具是 asyncio.create_task 与 asyncio.gather。本文将从执行原理出发,通过完整的代码示例、机制拆解和结构对比,讲解如何正确使用这两个函数并发调度多个异步任务,并规避常见的内存回收与异常捕获陷阱。

Python asyncio并发执行多个任务:create_task与gather用法示例
图 1:串行阻塞模型与 asyncio 协作式并发耗时对比

一、理解并发前提:直接 await 为什么不是并发?

很多初学者在编写 asyncio 代码时,容易写出如下形式的代码:

# 常见误区:表面上使用了 async/await,但实际上是串行执行
async def main():
    res1 = await fetch_data("A", 1)  # 等待 A 完成后才继续向下执行
    res2 = await fetch_data("B", 2)  # 等待 B 完成后才继续向下执行
    res3 = await fetch_data("C", 3)  # 等待 C 完成后才继续向下执行

在这段代码中,每一次 await 都会立刻暂停 main 函数的执行,直到对应的协程完全执行完毕返回结果。事件循环虽然在这期间可以执行其他任务,但由于当前 main 并没有在事件循环中注册后续任务,导致任务 B 必须在任务 A 结束后才开始创建并进入循环,实际总耗时依旧是各个任务耗时的累加。要实现真正的“同时开工、重叠等待”,必须将多个协程在当前事件循环中完成调度注册。

二、asyncio.create_task:立即调度单个任务

asyncio.create_task(coro) 接收一个协程对象,并将其包装为 asyncio.Task 对象。调用该函数的瞬间,任务就已经被排入了事件循环的执行队列。这意味着该任务在下一次循环迭代时即可立即并发运行,而不需要等到你主动 await 它。

1. 基础用法与代码示例

以下示例展示了如何使用 asyncio.create_task 派生多个并发任务:

import asyncio
import time

async def worker(name: str, delay: int) -> str:
    print(f"[{time.strftime('%X')}] 任务 {name} 开始执行,预计耗时 {delay} 秒")
    await asyncio.sleep(delay)
    print(f"[{time.strftime('%X')}] 任务 {name} 执行完毕")
    return f"{name}_done"

async def main():
    start_time = time.perf_counter()

    # 1. 创建 Task 对象:瞬间完成事件循环排期,后台开始并发推进
    task1 = asyncio.create_task(worker("A", 1))
    task2 = asyncio.create_task(worker("B", 2))
    task3 = asyncio.create_task(worker("C", 3))

    # 2. 在需要结果的时候再分别 await 收集
    res1 = await task1
    res2 = await task2
    res3 = await task3

    elapsed = time.perf_counter() - start_time
    print(f"[{time.strftime('%X')}] 所有任务完成,返回值: {[res1, res2, res3]}")
    print(f"总耗时: {elapsed:.2f} 秒")

if __name__ == "__main__":
    asyncio.run(main())

运行输出(示例):

[10:00:00] 任务 A 开始执行,预计耗时 1 秒
[10:00:00] 任务 B 开始执行,预计耗时 2 秒
[10:00:00] 任务 C 开始执行,预计耗时 3 秒
[10:00:01] 任务 A 执行完毕
[10:00:02] 任务 B 执行完毕
[10:00:03] 任务 C 执行完毕
[10:00:03] 所有任务完成,返回值: ['A_done', 'B_done', 'C_done']
总耗时: 3.01 秒
关键注意点(Python 3.8+ 垃圾回收陷阱):
官方文档明确指出,后台由 create_task 创建的任务若未被变量强引用(例如直接调用 asyncio.create_task(background_work()) 而未将返回值赋给变量),可能在执行过程中被垃圾回收机制(GC)销毁导致任务中断。因此,如果任务是在后台运行且暂时不立即 await,应将其添加到一个全局或外部活跃的作用域集合中(如 background_tasks.add(task)),并在其完成时从集合移除(如通过 task.add_done_callback(background_tasks.discard))。
Python asyncio并发执行多个任务:create_task与gather用法示例
图 2:create_task 与 gather 在事件循环中的流转机制

三、asyncio.gather:批量并发与结果聚合

如果你的场景是拥有一个确定的任务列表(如并发抓取 10 个网页),并且需要等待它们全部执行完毕后统一收集返回结果,那么 asyncio.gather 通常是更简洁优雅的选择。

gather(*aws, return_exceptions=False) 可以接收多个可等待对象(协程对象或已经创建的 Task)。如果传入的是原生协程对象,gather 会在内部自动将其转换为 Task 并投入调度。完成时,它将按照传入任务的先后顺序,返回一个包含所有结果的列表(List)。

1. 批量调度与解包传参

在实际工程中,我们通常将一组参数映射为一个任务列表,并通过星号 * 进行解包传入:

import asyncio
import time

async def fetch_url(url_id: int) -> dict:
    await asyncio.sleep(1.5)  # 模拟网络延迟
    return {"id": url_id, "status": 200}

async def main():
    start = time.perf_counter()

    # 构造多个协程调用
    tasks = [fetch_url(i) for i in range(1, 6)]

    # 并发执行并解包,按序等待所有结果
    results = await asyncio.gather(*tasks)

    print(f"聚合返回结果: {results}")
    print(f"并发执行耗时: {time.perf_counter() - start:.2f} 秒")

if __name__ == "__main__":
    asyncio.run(main())

运行输出(示例):

聚合返回结果: [{'id': 1, 'status': 200}, {'id': 2, 'status': 200}, {'id': 3, 'status': 200}, {'id': 4, 'status': 200}, {'id': 5, 'status': 200}]
并发执行耗时: 1.51 秒

2. 异常隔离机制:return_exceptions 参数

当多个并发任务中某一个任务发生未捕获异常时,asyncio.gather 的默认行为是立即将该异常向上抛出中断当前的 await 调用。但此时,其余尚未完成的任务并不会自动被取消,它们依然会在后台继续默默运行。

为了避免单点失败影响整体,可设置 return_exceptions=True。此时异常对象会被作为对应位置的普通返回值存放在结果列表中,而不会中断整个批处理流程:

async def faulty_worker(task_id: int):
    await asyncio.sleep(1)
    if task_id == 2:
        raise ValueError(f"任务 {task_id} 发生未知错误")
    return f"任务 {task_id} 成功"

async def main():
    tasks = [faulty_worker(i) for i in range(1, 4)]
    
    # 关键参数:return_exceptions=True
    results = await asyncio.gather(*tasks, return_exceptions=True)

    for i, res in enumerate(results, start=1):
        if isinstance(res, Exception):
            print(f"[{i}] 失败,捕获到异常: {res}")
        else:
            print(f"[{i}] 成功,结果为: {res}")

if __name__ == "__main__":
    asyncio.run(main())
Python asyncio并发执行多个任务:create_task与gather用法示例
图 3:create_task 与 gather 的关键差异与特性矩阵

四、两者结合应用:控制高并发速率

在实际生产环境中,盲目使用 gather 并发上千个请求极易导致本地文件描述符耗尽(Too many open files)或击垮对端服务器。最佳实践是将 asyncio.Semaphore(信号量)与 create_task 或 gather 结合,限制同时处于运行状态的最大协程数:

import asyncio
import time

# 限制最大同时并发数不超过 3
sem = asyncio.Semaphore(3)

async def limited_fetch(item_id: int):
    async with sem:
        print(f"[{time.strftime('%X')}] 正在处理 item-{item_id} (已获取信号量)")
        await asyncio.sleep(2)  # 模拟网络延迟
        return f"item-{item_id}-data"

async def main():
    # 虽然创建了 9 个任务,但由于 Semaphore 约束,每次只有 3 个在并发执行
    tasks = [limited_fetch(i) for i in range(1, 10)]
    results = await asyncio.gather(*tasks)
    print(f"全部完成,抓取数量: {len(results)}")

if __name__ == "__main__":
    asyncio.run(main())
Python asyncio并发执行多个任务:create_task与gather用法示例
图 4:Python asyncio 并发任务架构选型决策指引

五、总结与速查清单

掌握 asyncio 并发执行的核心在于厘清调度时机与结果等待的分离:

  • 必须避免伪并发:直接逐行 await coro() 仍然是同步串行执行,没有任何重叠等待的效益。
  • 单个任务与后台作业用 create_task:调用时即在事件循环挂号,允许在后续任意位置 await;但务必保留任务对象引用,防止被垃圾回收中途终止。
  • 确定批次与结果汇聚用 gather:将多个任务一并等待,并按输入顺序返回结果;善用 return_exceptions=True 实现容错隔离,防止单任务崩溃摧毁整个批次。
  • 保护对端资源:高并发并非无节制,针对大量并发任务,必须使用 asyncio.Semaphore 设定并发上限窗口。
相关专题:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。