引言:为什么我们需要异步编程?

你是否曾因程序在等待网络请求或文件读写时“卡住”而烦恼?在传统的同步编程中,一旦遇到I/O(输入/输出)操作,整个程序就会停下来等待,导致CPU资源被白白浪费,效率低下。这就是异步编程要解决的核心痛点。

Python的asyncio库提供了一套优雅的解决方案——事件循环(Event Loop)。它允许程序在等待某个任务时,自动切换到其他任务继续执行,从而让多个任务“同时”推进,大幅提升程序的吞吐量。本文将带你从同步阻塞的困境出发,一步步掌握asyncio的核心用法与最佳实践。

同步编程的困境:一个直观的例子

让我们先看一个典型的同步程序。假设我们有四个任务,每个任务都需要模拟一段I/O等待时间。

import time

def task1():
    time.sleep(5)  # 模拟5秒的I/O操作
    return 10

def task2():
    time.sleep(10) # 模拟10秒的I/O操作
    return 20

def task3():
    time.sleep(15) # 模拟15秒的I/O操作
    return 30

def task4():
    time.sleep(20) # 模拟20秒的I/O操作
    return 40

def main():
    res = task1()
    print(f"程序执行的结果为{res}")
    res = task2()
    print(f"程序执行的结果为{res}")

if __name__ == '__main__':
    starttime = time.time()
    main()
    endtime = time.time()
    Time_consuming = endtime - starttime
    print(f"消耗的时间为{Time_consuming}")

代码说明

  • time.sleep() 函数模拟了耗时的I/O操作。
  • 任务按顺序执行:task1 完成后才执行 task2
  • if __name__ == '__main__': 是Python的常见入口判断,确保模块在被直接运行时才执行主逻辑,而被导入时不会自动执行。

输出结果

程序执行的结果为10
程序执行的结果为20
消耗的时间为15.00128960609436

程序总共执行了约15秒。绝大部分时间都花在了等待time.sleep()上。在真实场景中,这可能是等待数据库响应、网络请求返回或文件读写完成。CPU在此期间几乎处于闲置状态,这是对计算资源的巨大浪费。

异步编程的核心:事件循环与协程

异步编程的核心思想是:当一个任务需要等待时,CPU不应该傻等,而是去执行其他就绪的任务。Python通过asyncio库实现了这一机制。

1. 定义异步函数

异步函数使用 async def 关键字定义,表明这个函数可以被事件循环调度。

async def my_task():
    # 异步函数体
    pass

2. 等待异步操作

在异步函数内部,使用 await 关键字来等待一个异步操作完成。await 会告诉事件循环:“这里可以挂起我,先去执行别的任务吧”。

关键点await 后面必须跟一个“可等待对象”(Awaitable),例如另一个异步函数调用,或者 asyncio.sleep()。直接使用 time.sleep() 是无效的,因为它是同步阻塞函数。

3. 创建与运行事件循环

事件循环是异步任务的调度中心。在Python 3.7+中,最简单的启动方式是使用 asyncio.run()

import asyncio

async def main():
    print("Hello")
    await asyncio.sleep(1)
    print("World")

if __name__ == '__main__':
    asyncio.run(main())  # 创建事件循环并运行main函数

第一个异步程序:修复常见错误

让我们尝试将上面的同步示例改为异步版本。初学者常犯两个错误:1) 错误地创建事件循环;2) 在异步函数中使用了同步阻塞调用。

错误示例

import asyncio
import time

async def task1():
    print("task1开始")
    time.sleep(5)  # ❌ 错误!这是同步阻塞调用
    time.sleep(5)
    await asyncio.sleep(5)  # ✅ 正确的异步等待
    time.sleep(5)  # ❌ 错误!
    print("task1结束")
    return 10

async def task2():
    print("task2开始")
    time.sleep(10)  # ❌ 错误!
    print("task2结束")
    return 20

async def task3():
    print("task3开始")
    time.sleep(15)  # ❌ 错误!
    print("task3结束")
    return 30

async def task4():
    print("task4开始")
    await asyncio.sleep(20)  # ✅ 正确
    print("task4结束")
    return 40

async def main():
    print("main函数开始")
    event_loop = asyncio.get_running_loop()  # 获取当前运行的事件循环
    t1 = event_loop.create_task(task1())
    t2 = event_loop.create_task(task2())
    t3 = event_loop.create_task(task3())
    t4 = event_loop.create_task(task4())
    
    result = await t1
    print(f"现在的结果是{result}")
    result = await t2
    print(f"现在的结果是{result}")
    result = await t3
    print(f"现在的结果是{result}")
    result = await t4
    print(f"现在的结果是{result}")
    print("main函数结束")

if __name__ == '__main__':
    starttime = time.time()
    event_loop = asyncio.get_event_loop()  # ⚠️ Python 3.10+ 会发出弃用警告
    event_loop.run_until_complete(main())
    endtime = time.time()
    Time_consuming = endtime - starttime
    print(f"消耗的时间为{Time_consuming}")

执行结果与问题分析

DeprecationWarning: There is no current event loop
  event_loop = asyncio.get_event_loop()
main函数开始
task1开始
task2开始
task2结束
task3开始
task3结束
task4开始
task1结束
现在的结果是10
现在的结果是20
现在的结果是30
task4结束
现在的结果是40
main函数结束
消耗的时间为55.01034593582153

这段代码有两个主要问题:

  1. 弃用警告:在Python 3.10+中,asyncio.get_event_loop()在没有当前事件循环时会发出弃用警告。推荐使用asyncio.run()
  2. 同步阻塞task1task2task3中使用了time.sleep(),这会导致整个事件循环被阻塞,失去了异步的优势。总执行时间高达55秒,比同步版本还慢!

正确的异步实现

将所有time.sleep()替换为await asyncio.sleep(),并使用asyncio.run()来启动。

import asyncio
import time

async def task1():
    print("task1开始")
    await asyncio.sleep(5)  # ✅ 全部改为异步等待
    await asyncio.sleep(5)
    await asyncio.sleep(5)
    await asyncio.sleep(5)
    print("task1结束")
    return 10

async def task2():
    print("task2开始")
    await asyncio.sleep(10)  # ✅
    print("task2结束")
    return 20

async def task3():
    print("task3开始")
    await asyncio.sleep(3)  # ✅
    print("task3结束")
    return 30

async def task4():
    print("task4开始")
    await asyncio.sleep(2)  # ✅
    print("task4结束")
    return 40

async def main():
    print("main函数开始")
    event_loop = asyncio.get_running_loop()
    t1 = event_loop.create_task(task1())
    t2 = event_loop.create_task(task2())
    t3 = event_loop.create_task(task3())
    t4 = event_loop.create_task(task4())
    
    result = await t1
    print(f"现在的结果是{result}")
    result = await t2
    print(f"现在的结果是{result}")
    result = await t3
    print(f"现在的结果是{result}")
    result = await t4
    print(f"现在的结果是{result}")
    print("main函数结束")

if __name__ == '__main__':
    starttime = time.time()
    asyncio.run(main())  # ✅ 使用推荐的方式启动
    endtime = time.time()
    Time_consuming = endtime - starttime
    print(f"消耗的时间为{Time_consuming}")

输出结果

main函数开始
task1开始
task2开始
task3开始
task4开始
task4结束
task3结束
task2结束
task1结束
现在的结果是10
现在的结果是20
现在的结果是30
现在的结果是40
main函数结束
消耗的时间为20.020315170288086

性能提升:总时间从55秒降低到约20秒!这是因为所有任务几乎同时开始,asyncio.sleep()是非阻塞的,事件循环可以在一个任务等待时切换到其他任务。

注意任务的结束顺序:task4(2秒)最先结束,然后是task3(3秒),接着是task2(10秒),最后是task1(20秒)。这正是异步并发的效果。

Jupyter Notebook 中的特殊处理

在Jupyter Notebook或VS Code的交互式环境中运行上述代码可能会遇到错误:

RuntimeError: This event loop is already running

原因:Notebook环境本身已经运行着一个事件循环。asyncio.run()会尝试创建新的事件循环,与现有循环冲突。

解决方案:在Notebook中,应该直接使用await来运行异步函数,或者使用Notebook专用的异步执行方式。

# 在Jupyter Notebook单元格中直接运行
await main()

或者使用asyncio.create_task()await的组合。

更优雅的写法:使用 asyncio.gather()

手动创建和管理多个任务比较繁琐。asyncio.gather()可以一次性并发运行多个协程,并收集它们的结果。

import asyncio
import time

async def task1():
    print("task1开始")
    await asyncio.sleep(5)
    await asyncio.sleep(5)
    await asyncio.sleep(5)
    await asyncio.sleep(5)
    print("task1结束")
    return 10

async def task2():
    print("task2开始")
    await asyncio.sleep(10)
    print("task2结束")
    return 20

async def task3():
    print("task3开始")
    await asyncio.sleep(3)
    print("task3结束")
    return 30

async def task4():
    print("task4开始")
    await asyncio.sleep(2)
    print("task4结束")
    return 40

async def main():
    print("main函数开始")
    # 使用gather并发执行所有任务
    result = await asyncio.gather(task1(), task2(), task3(), task4())
    print(f"所有任务的结果为: {result}")

if __name__ == '__main__':
    starttime = time.time()
    asyncio.run(main())
    endtime = time.time()
    Time_consuming = endtime - starttime
    print(f"消耗的时间为{Time_consuming}")

输出

main函数开始
task1开始
task2开始
task3开始
task4开始
task4结束
task3结束
task2结束
task1结束
所有任务的结果为: [10, 20, 30, 40]
消耗的时间为20.02048420906067

优势

  1. 代码更简洁:一行代码替代了多个create_task()await
  2. 结果收集更方便gather()返回一个包含所有任务结果的列表,顺序与传入顺序一致。
  3. 自动错误处理:可以统一处理所有任务的异常。

总结与最佳实践

通过本文的对比,我们清晰地看到了异步编程如何将程序执行时间从55秒优化到20秒,提升了近2.75倍的效率。以下是关键要点:

  1. 识别I/O密集型场景:当程序有大量等待操作(网络请求、文件读写、数据库查询)时,考虑使用异步编程。

  2. 正确使用异步函数

    • 使用async def定义异步函数
    • 在函数内部使用await等待异步操作
    • 避免在异步函数中使用同步阻塞调用(如time.sleep()
  3. 选择正确的启动方式

    • 普通脚本:使用asyncio.run(main())
    • Jupyter/交互环境:直接使用await main()
  4. 简化多任务管理:优先使用asyncio.gather()来并发执行多个任务,而不是手动创建和管理每个任务。

  5. 理解事件循环:事件循环是异步编程的引擎,它负责在任务之间智能切换,最大化CPU利用率。

异步编程虽然需要思维模式的转变,但一旦掌握,就能显著提升程序的性能和响应能力。尤其是在Web服务器、爬虫、数据处理等I/O密集型应用中,异步技术几乎是必备技能。

希望这篇教程能帮助你顺利踏入Python异步编程的大门。如果你在实践中遇到问题,欢迎在评论区留言讨论!

觉得有帮助?点赞收藏支持一下,让更多小伙伴看到这篇干货!

更多推荐