深入理解 Python 的 async 和 await
在现代软件开发中,异步编程已成为提高应用程序性能的关键手段,尤其是在处理 I/O 密集型任务时。Python 提供了强大的 async
和 await
关键字,使得编写异步代码更加直观和高效。在本文中,我们将深入探讨 Python 中的 async
和 await
,结合实例、协程的概念,以及底层原理。
一、协程的概念
1.1 什么是协程?
协程(Coroutine)是一种 协作式的并发计算。与传统的多线程并发不同,协程通过主动让出执行权来实现任务的切换,而不是依赖操作系统调度。
在 Python 中,协程是由 协程函数 定义的,并通过 async def
关键字实现。协程不会立即执行,而是返回一个协程对象,表示任务的执行逻辑和状态。协程对象的执行需要借助 事件循环 来调度。
1.2 协程的优点
二、async
和 await
的基础用法
2.1 定义协程函数
协程函数是使用 async def
定义的函数,调用它时不会立即执行,而是返回一个协程对象。
import asyncio
# 定义一个协程函数
async def say_hello():
print("Hello!")
await asyncio.sleep(1) # 模拟异步等待
print("Goodbye!")
2.2 调用和执行协程
协程需要通过 await
或事件循环来执行。直接调用协程函数只会返回一个协程对象。
# 错误示例:直接调用协程函数
coro = say_hello()
print(coro) # <coroutine object say_hello at 0x...>
# 正确示例:通过 asyncio.run 执行协程
asyncio.run(say_hello())
2.3 await
的作用
await
关键字用于暂停协程的执行,等待一个可等待对象完成,并返回结果。可等待对象包括协程对象、asyncio
提供的异步操作(如 asyncio.sleep
)等。
async def delayed_hello():
print("Waiting for 2 seconds...")
await asyncio.sleep(2)
print("Done!")
示例:
import asyncio
# 定义一个协程函数
async def say_hello():
print("Hello!")
await asyncio.sleep(1) # 模拟异步等待
print("Goodbye!")
async def say_hi():
print("Waiting for 2 seconds...")
await asyncio.sleep(2)
print("Done!")
async def say():
await say_hello()
await say_hi()
asyncio.run(say())
三、协程的原理与事件循环
3.1 协程对象是什么?
当调用一个协程函数时,它不会执行,而是返回一个 协程对象。协程对象保存了协程的执行逻辑和当前状态,只有在事件循环中调度时才会真正运行。
import asyncio
async def example():
await asyncio.sleep(1)
coro = example()
print(type(coro)) # <class 'coroutine'>
可以使用 asyncio.iscoroutine()
来判断某个对象是否为协程对象。
3.2 事件循环
事件循环是 asyncio
模块的核心,用于管理和调度协程的执行。asyncio.run()
是高层的事件循环管理器,它会:
- 创建一个新的事件循环。
- 执行传入的协程,调度所有任务。
- 关闭事件循环,清理资源。
示例:
async def task1():
print("Task 1 started")
await asyncio.sleep(1)
print("Task 1 completed")
async def task2():
print("Task 2 started")
await asyncio.sleep(2)
print("Task 2 completed")
async def main():
await asyncio.gather(task1(), task2()) # 并发执行多个任务
# 启动事件循环
asyncio.run(main())
输出:
Task 1 started
Task 2 started
Task 1 completed
Task 2 completed
3.3 asyncio.run()
的底层原理
asyncio.run()
的核心逻辑如下:
- 检查当前线程是否已有运行的事件循环,防止嵌套。
- 创建新的事件循环并将协程添加到其中。
- 使用
loop.run_until_complete()
执行协程,阻塞主线程直到完成。 - 关闭事件循环,释放资源。
代码示意:
def asyncio_run(coro):
loop = asyncio.new_event_loop() # 创建新的事件循环
asyncio.set_event_loop(loop)
try:
return loop.run_until_complete(coro) # 执行协程
finally:
loop.close() # 关闭事件循环
四、实例:并发处理多个任务
假设我们需要同时处理多个异步任务,例如向多个 API 发送请求。可以使用 asyncio.gather
来并发执行多个协程。
import asyncio
async def fetch_data(api):
print(f"Fetching data from {api}...")
await asyncio.sleep(2) # 模拟网络延迟
print(f"Data from {api} received!")
async def main():
apis = ["API_1", "API_2", "API_3"]
# 并发执行多个协程
await asyncio.gather(*(fetch_data(api) for api in apis))
asyncio.run(main())
输出:
Fetching data from API_1...
Fetching data from API_2...
Fetching data from API_3...
Data from API_1 received!
Data from API_2 received!
Data from API_3 received!
五、注意事项与最佳实践
5.1 注意事项
- 不能嵌套调用
asyncio.run()
:asyncio.run()
会创建一个新的事件循环,嵌套调用会引发RuntimeError
。 - 协程不能直接调用:协程必须通过
await
或事件循环执行,直接调用只会返回协程对象。 - 事件循环的生命周期:确保事件循环在程序退出前正确关闭,防止资源泄漏。
5.2 最佳实践
asyncio.run()
作为程序的入口,统一管理事件循环。asyncio.gather()
或 asyncio.create_task()
并发执行多个任务。六、总结
Python 的 async
和 await
提供了一种优雅的方式来实现异步编程。通过协程和事件循环,开发者可以高效地处理 I/O 密集型任务,而无需引入复杂的线程或进程管理。
作者:花千树-010