2. 并发执行多个协程

任务说明

本案例要求使用 asyncio.gather 并发运行多个协程,通过观察执行顺序和总耗时,理解并发与并行的区别。我们将创建三个模拟耗时操作的协程,每个协程内部通过 asyncio.sleep 模拟 I/O 等待,然后使用 gather 同时启动它们,记录总耗时并与串行执行对比。

核心实现

import asyncio
import time

async def worker(name: str, delay: float) -> str:
    """模拟一个耗时协程:等待 delay 秒后返回结果"""
    print(f"协程 {name} 开始,预计等待 {delay} 秒")
    await asyncio.sleep(delay)
    print(f"协程 {name} 完成")
    return f"{name} 的结果"

async def main():
    # 记录开始时间
    start = time.perf_counter()

    # 并发执行三个协程
    results = await asyncio.gather(
        worker("A", 2),
        worker("B", 1),
        worker("C", 3)
    )

    # 计算总耗时
    elapsed = time.perf_counter() - start

    print(f"并发执行结果: {results}")
    print(f"并发总耗时: {elapsed:.2f} 秒")

    # 对比:串行执行同样三个协程
    start = time.perf_counter()
    serial_results = []
    for name, delay in [("A", 2), ("B", 1), ("C", 3)]:
        serial_results.append(await worker(name, delay))
    elapsed_serial = time.perf_counter() - start

    print(f"串行执行结果: {serial_results}")
    print(f"串行总耗时: {elapsed_serial:.2f} 秒")

    # 输出对比结论
    print(f"\n耗时对比:并发 {elapsed:.2f}s vs 串行 {elapsed_serial:.2f}s")
    print("并发总耗时约等于最慢协程的耗时(3秒),而非三者之和(6秒)")

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

执行流程说明

代码中 worker 协程通过 asyncio.sleep 模拟 I/O 等待(如网络请求、文件读写)。asyncio.gather 接收三个协程对象,将它们同时调度到事件循环中。由于 sleep 会主动让出控制权,三个协程的等待时间在事件循环中重叠推进,因此总耗时取决于最慢的协程(C,3秒),而非三者耗时之和。

串行对比部分使用普通 for 循环依次 await 每个协程,前一个完成后才启动下一个,因此总耗时为三者之和(6秒)。通过对比可以直观看到:并发让多个 I/O 等待重叠,但并未真正同时执行计算任务——这正是并发(交替执行)与并行(多核同时执行)的核心区别。

运行与验证

  1. 保存代码为 gather_demo.py,在 Python 3.7+ 环境运行:python gather_demo.py
  2. 观察输出中的执行顺序:三个协程的“开始”打印会连续出现(几乎同时),而“完成”打印按各自延迟时间先后出现(B先于A,A先于C)。
  3. 判定标准:
    - 并发总耗时约为 3 秒(最慢协程耗时),串行总耗时约为 6 秒;
    - 若并发耗时接近 6 秒,说明协程未真正并发(可能误用了同步阻塞操作);
    - 若“开始”打印不是连续出现,而是逐个间隔出现,说明 gather 未正确并发调度。
asyncio.gather 并发调度时序 t=0s t=3s 1s 2s 协程A (sleep 2s) t=2s 完成 协程B (sleep 1s) t=1s 完成 协程C (sleep 3s) t=3s 完成 事件循环调度原理: 1. gather 将三个协程注册到事件循环,按顺序启动(打印"开始") 2. 每个协程遇到 await sleep 时让出控制权,事件循环切换到下一个协程 3. 等待时间重叠推进,总耗时 = max(2s, 1s, 3s) = 3s,而非 2+1+3=6s

预期输出

协程 A 开始,预计等待 2 秒
协程 B 开始,预计等待 1 秒
协程 C 开始,预计等待 3 秒
协程 B 完成
协程 A 完成
协程 C 完成
并发执行结果: ['A 的结果', 'B 的结果', 'C 的结果']
并发总耗时: 3.00 秒
协程 A 开始,预计等待 2 秒
协程 A 完成
协程 B 开始,预计等待 1 秒
协程 B 完成
协程 C 开始,预计等待 3 秒
协程 C 完成
串行执行结果: ['A 的结果', 'B 的结果', 'C 的结果']
串行总耗时: 6.00 秒

耗时对比:并发 3.00s vs 串行 6.00s
并发总耗时约等于最慢协程的耗时(3秒),而非三者之和(6秒)

注意:上述输出为基于代码逻辑的确定性预期。实际运行中,perf_counter 的精度和系统调度可能带来毫秒级误差,但并发约 3 秒、串行约 6 秒的对比关系稳定成立。三个协程的“开始”打印在并发模式下会连续出现(间隔极小),而“完成”打印按延迟时间排序出现。