python 中只使用async和await关键字,不使用Task,有什么缺陷?
2024年10月6日大约 5 分钟
python 中只使用async和await关键字,不使用Task,有什么缺陷?
在 Python 的异步编程中,async
和 await
关键字用于定义和调用协程。但如果只使用这两个关键字,而不使用 asyncio.create_task()
或其他方式来创建任务(Task
),会有以下缺陷:
1. 缺乏并发执行
问题描述
- 顺序执行:直接使用
await
调用协程会导致协程按顺序执行,后一个协程会等待前一个协程完成。 - 无法充分利用异步优势:异步编程的主要优势在于能够并发处理 I/O 操作,如果不创建任务,就无法实现真正的并发。
示例
import asyncio
async def task1():
print("任务 1 开始")
await asyncio.sleep(2)
print("任务 1 完成")
async def task2():
print("任务 2 开始")
await asyncio.sleep(2)
print("任务 2 完成")
async def main():
await task1()
await task2()
asyncio.run(main())
输出:
任务 1 开始
任务 1 完成
任务 2 开始
任务 2 完成
解释:
task1
和task2
按顺序执行,总耗时约 4 秒。
解决方案
使用 asyncio.create_task()
创建任务,实现并发执行:
async def main():
task1_coro = asyncio.create_task(task1())
task2_coro = asyncio.create_task(task2())
await task1_coro
await task2_coro
输出:
任务 1 开始
任务 2 开始
任务 1 完成
任务 2 完成
解释:
task1
和task2
并发执行,总耗时约 2 秒。
2. 无法后台运行协程
问题描述
- 无法在等待其他操作的同时运行协程:如果直接使用
await
,主协程会等待子协程完成,无法同时执行其他任务。 - 缺乏灵活性:在复杂的程序中,可能需要在后台运行一些任务,而不阻塞主协程。
示例
async def background_task():
while True:
print("后台任务运行中...")
await asyncio.sleep(1)
async def main():
await background_task() # 这将阻塞主协程
asyncio.run(main())
解释:
- 主协程被
background_task
阻塞,无法执行其他操作。
解决方案
使用 asyncio.create_task()
创建后台任务:
async def main():
task = asyncio.create_task(background_task())
# 主协程可以继续执行其他操作
await asyncio.sleep(5) # 主协程等待一段时间
task.cancel() # 取消后台任务
3. 无法捕获协程中的异常
问题描述
- 异常处理困难:直接使用
await
时,若协程中发生异常,会立即传播,可能导致整个程序崩溃。 - 无法监控任务状态:没有任务对象,无法检查协程的执行状态或结果。
示例
async def faulty_task():
await asyncio.sleep(1)
raise ValueError("任务出错了")
async def main():
await faulty_task() # 异常会在这里抛出
asyncio.run(main())
输出:
ValueError: 任务出错了
解决方案
使用任务对象捕获异常:
async def main():
task = asyncio.create_task(faulty_task())
try:
await task
except ValueError as e:
print(f"捕获到异常:{e}")
输出:
捕获到异常:任务出错了
4. 无法同时等待多个协程的完成
问题描述
- 缺乏批量管理:直接使用
await
无法方便地等待多个协程的完成,特别是在协程列表中。
示例
async def worker(n):
await asyncio.sleep(n)
print(f"工作者 {n} 完成")
async def main():
await worker(1)
await worker(2)
await worker(3)
asyncio.run(main())
输出:
工作者 1 完成
工作者 2 完成
工作者 3 完成
解释:
- 总耗时约 6 秒,协程按顺序执行。
解决方案
使用 asyncio.gather()
并发等待多个协程:
async def main():
await asyncio.gather(worker(1), worker(2), worker(3))
输出:
工作者 1 完成
工作者 2 完成
工作者 3 完成
解释:
- 总耗时约 3 秒,协程并发执行。
5. 阻塞事件循环,降低性能
问题描述
- 阻塞事件循环:如果协程中有长时间运行的同步代码,且未妥善处理,会阻塞事件循环,影响其他协程的执行。
示例
async def long_sync_task():
print("开始耗时的同步任务")
import time
time.sleep(5) # 同步阻塞
print("同步任务完成")
async def main():
await long_sync_task()
asyncio.run(main())
问题:
time.sleep(5)
是同步阻塞,会阻塞整个事件循环。
解决方案
使用线程池执行阻塞的同步代码:
async def long_sync_task(): print("开始耗时的同步任务") await asyncio.to_thread(time.sleep, 5) print("同步任务完成")
使用任务管理其他协程:通过任务,可以更好地管理协程的执行,防止阻塞事件循环。
总结
- 无法实现并发:不使用
Task
,协程将顺序执行,无法充分利用异步编程的优势。 - 缺乏任务管理:无法跟踪、取消或处理协程的状态和结果。
- 异常处理受限:无法捕获后台运行的协程中的异常。
- 阻塞主协程:无法在等待某个协程的同时执行其他操作。
建议
- 使用
asyncio.create_task()
:将协程封装为任务,允许并发执行,并提供对任务的控制和管理。 - 使用
asyncio.gather()
:并发地运行多个协程,并等待它们全部完成。 - 注意阻塞调用:确保协程内部的操作是非阻塞的,避免阻塞事件循环。
示例:改进后的并发执行
import asyncio
async def fetch_data(n):
print(f"开始获取数据 {n}")
await asyncio.sleep(n)
print(f"数据 {n} 获取完成")
return f"数据 {n}"
async def main():
tasks = [asyncio.create_task(fetch_data(i)) for i in range(1, 4)]
results = await asyncio.gather(*tasks)
print("所有数据已获取:", results)
asyncio.run(main())
输出:
开始获取数据 1
开始获取数据 2
开始获取数据 3
数据 1 获取完成
数据 2 获取完成
数据 3 获取完成
所有数据已获取: ['数据 1', '数据 2', '数据 3']
解释:
fetch_data
函数被并发执行,减少了总的运行时间。- 使用
asyncio.create_task()
和asyncio.gather()
管理和等待多个任务。
通过正确地使用 Task
,可以充分发挥 Python 异步编程的优势,实现高效的并发操作。