释放 Python 并发编程的强大力量。 学习如何创建、管理和取消 Asyncio 任务,以构建高性能、可扩展的应用程序。
精通 Python Asyncio:深入解析任务创建与管理
在现代软件开发领域,性能至关重要。 应用程序需要具备快速响应能力,能够处理数千个并发网络连接、数据库查询和 API 调用,而不会出现任何问题。 对于 I/O 密集型操作——程序的大部分时间都花费在等待网络或磁盘等外部资源上——传统的同步代码可能会成为一个严重的瓶颈。 这就是异步编程的用武之地,而 Python 的 asyncio
库是释放这种力量的关键。
在 asyncio
并发模型的核心,是一个简单而强大的概念:Task(任务)。 虽然协程定义了要做什么,但 Task 实际上是完成事情的方式。 它们是并发执行的基本单元,允许您的 Python 程序同时处理多个操作,从而显著提高吞吐量和响应能力。
本综合指南将带您深入了解 asyncio.Task
。 我们将探索从创建的基础知识到高级管理模式、取消和最佳实践的所有内容。 无论您是构建高流量 Web 服务、数据抓取工具还是实时应用程序,掌握 Task 都是任何现代 Python 开发人员的一项基本技能。
什么是协程? 快速回顾
在我们能够跑之前,我们必须先走。 在 asyncio
的世界里,走就是理解协程。 协程是一种特殊类型的函数,使用 async def
定义。
当您调用常规 Python 函数时,它会从头到尾执行。 但是,当您调用协程函数时,它不会立即执行。 而是返回一个协程对象。 此对象是要完成的工作的蓝图,但它本身是惰性的。 它是可以启动、挂起和恢复的暂停计算。
import asyncio
async def say_hello(name: str):
print(f"Preparing to greet {name}...")
await asyncio.sleep(1) # Simulate a non-blocking I/O operation
print(f"Hello, {name}!")
# Calling the function doesn't run it, it creates a coroutine object
coro = say_hello("World")
print(f"Created a coroutine object: {coro}")
# To actually run it, you need to use an entry point like asyncio.run()
# asyncio.run(coro)
神奇的关键字是 await
。 它告诉事件循环,“此操作可能需要一段时间,因此请随时在此处暂停我,然后去处理其他事情。 当此操作完成时,请唤醒我。” 这种暂停和切换上下文的能力使并发成为可能。
并发的核心:理解 asyncio.Task
所以,协程是一个蓝图。 我们如何告诉厨房(事件循环)开始烹饪? 这就是 asyncio.Task
的用武之地。
asyncio.Task
是一个包装协程并在 asyncio 事件循环上安排其执行的对象。 这样想:
- 协程 (
async def
):一道菜的详细食谱。 - 事件循环:所有烹饪发生的核心厨房。
await my_coro()
:您站在厨房里,自己一步一步地按照食谱进行操作。 在菜肴完成之前,您什么都做不了。 这是顺序执行。asyncio.create_task(my_coro())
:您将食谱交给厨房里的厨师(Task),然后说:“开始处理这个。” 厨师立即开始,您可以自由地做其他事情,例如分发更多食谱。 这是并发执行。
关键的区别在于 asyncio.create_task()
安排协程“在后台”运行,并立即将控制权返回给您的代码。 您会获得一个 Task
对象,该对象充当正在进行的此操作的句柄。 您可以使用此句柄来检查其状态、取消它或稍后等待其结果。
创建您的第一个任务:`asyncio.create_task()` 函数
创建 Task 的主要方法是使用 asyncio.create_task()
函数。 它将协程对象作为参数,并安排其执行。
基本语法
用法很简单:
import asyncio
async def my_background_work():
print("Starting background work...")
await asyncio.sleep(2)
print("Background work finished.")
return "Success"
async def main():
print("Main function started.")
# Schedule my_background_work to run concurrently
task = asyncio.create_task(my_background_work())
# While the task runs, we can do other things
print("Task created. Main function continues to run.")
await asyncio.sleep(1)
print("Main function did some other work.")
# Now, wait for the task to complete and get its result
result = await task
print(f"Task completed with result: {result}")
asyncio.run(main())
请注意,输出如何显示 `main` 函数在创建任务后立即继续执行。 它不会阻塞。 只有当我们明确地在最后 `await task` 时才会暂停。
一个实际示例:并发 Web 请求
让我们通过一个常见的场景来了解 Task 的真正力量:从多个 URL 获取数据。 为此,我们将使用流行的 `aiohttp` 库,您可以使用 `pip install aiohttp` 安装它。
首先,让我们看看顺序(慢)的方式:
import asyncio
import aiohttp
import time
async def fetch_status(session, url):
async with session.get(url) as response:
return response.status
async def main_sequential():
urls = [
"https://www.python.org",
"https://www.google.com",
"https://www.github.com",
"https://www.microsoft.com"
]
start_time = time.time()
async with aiohttp.ClientSession() as session:
for url in urls:
status = await fetch_status(session, url)
print(f"Status for {url}: {status}")
end_time = time.time()
print(f"Sequential execution took {end_time - start_time:.2f} seconds")
# To run this, you would use: asyncio.run(main_sequential())
如果每个请求大约需要 0.5 秒,则总时间大约为 2 秒,因为每个 `await` 都会阻塞循环,直到该单个请求完成。
现在,让我们使用 Tasks 释放并发的力量:
import asyncio
import aiohttp
import time
# fetch_status coroutine remains the same
async def fetch_status(session, url):
async with session.get(url) as response:
return response.status
async def main_concurrent():
urls = [
"https://www.python.org",
"https://www.google.com",
"https://www.github.com",
"https://www.microsoft.com"
]
start_time = time.time()
async with aiohttp.ClientSession() as session:
# Create a list of tasks, but don't await them yet
tasks = [asyncio.create_task(fetch_status(session, url)) for url in urls]
# Now, wait for all tasks to complete
statuses = await asyncio.gather(*tasks)
for url, status in zip(urls, statuses):
print(f"Status for {url}: {status}")
end_time = time.time()
print(f"Concurrent execution took {end_time - start_time:.2f} seconds")
asyncio.run(main_concurrent())
当您运行并发版本时,您会看到显着差异。 总时间大约是最长单个请求的时间,而不是所有请求的总和。 这是因为一旦第一个 `fetch_status` 协程命中其 `await session.get(url)`,事件循环就会暂停它并立即启动下一个。 所有网络请求实际上都是同时发生的。
管理一组任务:基本模式
创建单个任务很棒,但在实际应用程序中,您经常需要启动、管理和同步整个组的任务。 `asyncio` 提供了几个强大的工具来实现此目的。
现代方法 (Python 3.11+):`asyncio.TaskGroup`
在 Python 3.11 中引入的 `TaskGroup` 是管理一组相关任务的新、推荐和最安全的方式。 它提供了所谓的结构化并发。
`TaskGroup` 的主要特点:
- 保证清理: `async with` 块在其中创建的所有任务完成之前不会退出。
- 强大的错误处理: 如果组中的任何任务引发异常,则会自动取消组中的所有其他任务,并且在退出 `async with` 块时会重新引发该异常(或 `ExceptionGroup`)。 这样可以防止孤立任务并确保可预测的状态。
以下是如何使用它:
import asyncio
async def worker(delay):
print(f"Worker starting, will sleep for {delay}s")
await asyncio.sleep(delay)
# This worker will fail
if delay == 2:
raise ValueError("Something went wrong in worker 2")
print(f"Worker with delay {delay} finished")
return f"Result from {delay}s"
async def main():
print("Starting main with TaskGroup...")
try:
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(worker(1))
task2 = tg.create_task(worker(2)) # This one will fail
task3 = tg.create_task(worker(3))
print("Tasks created in the group.")
# This part of the code will NOT be reached if an exception occurs
# The results would be accessed via task1.result(), etc.
print("All tasks completed successfully.")
except* ValueError as eg: # Note the `except*` for ExceptionGroup
print(f"Caught an exception group with {len(eg.exceptions)} exceptions.")
for exc in eg.exceptions:
print(f" - {exc}")
print("Main function finished.")
asyncio.run(main())
当您运行此代码时,您会看到 `worker(2)` 引发错误。 `TaskGroup` 捕获此错误,取消其他正在运行的任务(如 `worker(3)`),然后引发包含 `ValueError` 的 `ExceptionGroup`。 这种模式对于构建可靠的系统非常强大。
经典主力:`asyncio.gather()`
在 `TaskGroup` 之前,`asyncio.gather()` 是最常见的并发运行多个 awaitable 并等待它们全部完成的方式。
gather()
接受一系列协程或 Task,运行所有协程或 Task,并按与输入相同的顺序返回它们的结果列表。 对于“运行所有这些东西并给我所有结果”的常见情况,它是一个高级、方便的函数。
import asyncio
async def fetch_data(source, delay):
print(f"Fetching from {source}...")
await asyncio.sleep(delay)
return {"source": source, "data": f"some data from {source}"}
async def main():
# gather can take coroutines directly
results = await asyncio.gather(
fetch_data("API", 2),
fetch_data("Database", 3),
fetch_data("Cache", 1)
)
print(results)
asyncio.run(main())
使用 `gather()` 进行错误处理: 默认情况下,如果传递给 `gather()` 的任何 awaitable 引发异常,则 `gather()` 会立即传播该异常,并且其他正在运行的任务将被取消。 您可以使用 `return_exceptions=True` 更改此行为。 在此模式下,它不会引发异常,而是将其放置在结果列表中相应的位置。
# ... inside main()
results = await asyncio.gather(
fetch_data("API", 2),
asyncio.create_task(worker(1)), # This will raise a ValueError
fetch_data("Cache", 1),
return_exceptions=True
)
# results will contain a mix of successful results and exception objects
print(results)
细粒度控制:`asyncio.wait()`
asyncio.wait()` 是一个较低级别的函数,它提供对一组任务的更详细的控制。 与 `gather()` 不同,它不直接返回结果。 而是返回两组任务:`done` 和 `pending`。
它最强大的功能是 `return_when` 参数,它可以是:
asyncio.ALL_COMPLETED
(默认):当所有任务完成时返回。asyncio.FIRST_COMPLETED
:至少有一个任务完成时立即返回。asyncio.FIRST_EXCEPTION
:当任务引发异常时返回。 如果没有任务引发异常,则等效于 `ALL_COMPLETED`。
这对于查询多个冗余数据源并使用第一个响应的数据源等场景非常有用:
import asyncio
async def query_source(name, delay):
await asyncio.sleep(delay)
return f"Result from {name}"
async def main():
tasks = [
asyncio.create_task(query_source("Fast Mirror", 0.5)),
asyncio.create_task(query_source("Slow Main DB", 2.0)),
asyncio.create_task(query_source("Geographic Replica", 0.8))
]
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
# Get the result from the completed task
first_result = done.pop().result()
print(f"Got first result: {first_result}")
# We now have pending tasks that are still running. It's crucial to clean them up!
print(f"Cancelling {len(pending)} pending tasks...")
for task in pending:
task.cancel()
# Await the cancelled tasks to allow them to process the cancellation
await asyncio.gather(*pending, return_exceptions=True)
print("Cleanup complete.")
asyncio.run(main())
TaskGroup 与 gather() 与 wait():何时使用哪个?
- 使用 `asyncio.TaskGroup` (Python 3.11+) 作为您的默认选择。 其结构化并发模型对于管理属于单个逻辑操作的一组任务来说更安全、更干净且更不容易出错。
- 当您需要运行一组独立任务并只想获得其结果列表时,请使用 `asyncio.gather()`。 对于简单的情况,它仍然非常有用,并且在 Python 3.11 之前的版本中稍微简洁一些。
- 对于需要对完成条件进行细粒度控制(例如,等待第一个结果)并准备好手动管理剩余挂起任务的高级场景,请使用 `asyncio.wait()`。
任务生命周期和管理
创建 Task 后,您可以使用 `Task` 对象上的方法与其交互。
检查任务状态
task.done()
:如果任务已完成(成功、出现异常或被取消),则返回 `True`。task.cancelled()
:如果任务已取消,则返回 `True`。task.exception()
:如果任务引发异常,则返回异常对象。 否则,返回 `None`。 您只能在任务 `done()` 后调用此方法。
检索结果
获取任务结果的主要方法是简单地 `await task`。 如果任务成功完成,则返回该值。 如果它引发异常,则 `await task` 将重新引发该异常。 如果它被取消,则 `await task` 将引发 `CancelledError`。
或者,如果您知道任务已 `done()`,则可以调用 `task.result()`。 在返回值或引发异常方面,此方法的行为与 `await task` 完全相同。
取消的艺术
能够优雅地取消长时间运行的操作对于构建可靠的应用程序至关重要。 您可能需要由于超时、用户请求或系统中其他地方的错误而取消任务。
您可以通过调用其 task.cancel()
方法来取消任务。 但是,这不会立即停止任务。 而是安排在下一个 await
点在协程中引发 `CancelledError` 异常。 这是一个至关重要的细节。 它让协程有机会在退出前进行清理。
行为良好的协程应优雅地处理此 `CancelledError`,通常使用 `try...finally` 块来确保关闭文件句柄或数据库连接等资源。
import asyncio
async def resource_intensive_task():
print("Acquiring resource (e.g., opening a connection)...")
try:
for i in range(10):
print(f"Working... step {i+1}")
await asyncio.sleep(1) # This is an await point where CancelledError can be injected
except asyncio.CancelledError:
print("Task was cancelled! Cleaning up...")
raise # It's good practice to re-raise CancelledError
finally:
print("Releasing resource (e.g., closing connection). This always runs.")
async def main():
task = asyncio.create_task(resource_intensive_task())
# Let it run for a bit
await asyncio.sleep(2.5)
print("Main decides to cancel the task.")
task.cancel()
try:
await task
except asyncio.CancelledError:
print("Main has confirmed the task was cancelled.")
asyncio.run(main())
保证执行 `finally` 块,使其成为清理逻辑的理想场所。
使用 `asyncio.timeout()` 和 `asyncio.wait_for()` 添加超时
手动睡眠和取消很乏味。 `asyncio` 为这种常见模式提供了帮助程序。
在 Python 3.11+ 中,`asyncio.timeout()` 上下文管理器是首选方法:
async def long_running_operation():
await asyncio.sleep(10)
print("Operation finished")
async def main():
try:
async with asyncio.timeout(2): # Set a 2-second timeout
await long_running_operation()
except TimeoutError:
print("The operation timed out!")
asyncio.run(main())
对于 旧版本的 Python,您可以使用 `asyncio.wait_for()`。 它的工作方式类似,但将 awaitable 包装在函数调用中:
async def main_legacy():
try:
await asyncio.wait_for(long_running_operation(), timeout=2)
except asyncio.TimeoutError:
print("The operation timed out!")
asyncio.run(main_legacy())
这两种工具的工作方式都是在达到超时时取消内部任务,从而引发 `TimeoutError`(它是 `CancelledError` 的子类)。
常见陷阱和最佳实践
使用 Task 功能强大,但有一些常见的陷阱需要避免。
- 陷阱:“发射后不管”的错误。 使用 `create_task` 创建一个任务,然后从不等待它(或像 `TaskGroup` 这样的管理器)是很危险的。 如果该任务引发异常,则该异常可能会被静默丢失,并且您的程序可能会在任务完成其工作之前退出。 始终为每个负责等待其结果的任务指定一个明确的所有者。
- 陷阱:混淆 `asyncio.run()` 和 `create_task()`。 `asyncio.run(my_coro())` 是启动 `asyncio` 程序的主要入口点。 它创建一个新的事件循环并运行给定的协程直到完成。 `asyncio.create_task(my_coro())` 用于在已经运行的异步函数内部以安排并发执行。
- 最佳实践:对于现代 Python,使用 `TaskGroup`。 它的设计可以防止许多常见错误,例如被遗忘的任务和未处理的异常。 如果您使用的是 Python 3.11 或更高版本,请将其作为您的默认选择。
- 最佳实践:命名您的任务。 创建任务时,请使用 `name` 参数:`asyncio.create_task(my_coro(), name='DataProcessor-123')`。 这对于调试非常宝贵。 当您列出所有正在运行的任务时,具有有意义的名称可以帮助您了解程序正在做什么。
- 最佳实践:确保优雅关闭。 当您的应用程序需要关闭时,请确保您有一种机制来取消所有正在运行的后台任务,并等待它们正确清理。
高级概念:超越一瞥
对于调试和内省,`asyncio` 提供了一些有用的函数:
asyncio.current_task()
:返回当前正在执行的代码的 `Task` 对象。asyncio.all_tasks()
:返回事件循环当前管理的所有 `Task` 对象的集合。 这非常适合调试以查看正在运行的内容。
您还可以使用 `task.add_done_callback()` 将完成回调附加到任务。 虽然这可能很有用,但它通常会导致更复杂的回调式代码结构。 通常首选使用 `await`、`TaskGroup` 或 `gather` 的现代方法,以提高可读性和可维护性。
结论
`asyncio.Task` 是现代 Python 中并发的引擎。 通过了解如何创建、管理和优雅地处理任务的生命周期,您可以将 I/O 密集型应用程序从缓慢的顺序流程转换为高效、可扩展和响应迅速的系统。
我们已经介绍了从使用 `create_task()` 调度协程的基本概念到使用 `TaskGroup`、`gather()` 和 `wait()` 编排复杂工作流程的旅程。 我们还探讨了强大的错误处理、取消和超时对于构建弹性软件至关重要。
异步编程的世界是广阔的,但掌握 Task 是您可以采取的最重要的一步。 开始试验。 转换应用程序的顺序、I/O 密集型部分以使用并发任务,并亲身体验性能提升。 拥抱并发的力量,您将能够很好地构建下一代高性能 Python 应用程序。