引言
在Python开发中,I/O密集型任务(如网络请求、文件读写)常常成为性能瓶颈。传统的同步代码在等待I/O时会阻塞整个线程,导致资源浪费。虽然多线程可以缓解,但线程切换开销和GIL限制让Python的多线程在CPU密集型任务上力不从心。而异步编程通过事件循环和协程,让单个线程在等待I/O时切换到其他任务,极大提升并发性能。Python的asyncio库自3.4引入,已成为异步编程的标准。本文将从基础到实战,深入剖析asyncio的核心,并分享踩坑经验与最佳实践。
1. 核心概念:事件循环、协程、任务与Future
1.1 事件循环(Event Loop)
事件循环是asyncio的心脏,它负责调度协程、处理I/O事件。可以把它想象成一个不断循环的调度器:它检查是否有就绪的任务,执行它们,并处理它们产生的I/O事件。
在Python 3.7+,推荐使用asyncio.run()来启动事件循环,它会自动创建并关闭循环。
import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
asyncio.run(main())
1.2 协程(Coroutine)
协程是异步函数,使用async def定义。调用协程函数不会立即执行,而是返回一个协程对象。协程需要被事件循环调度才能执行。
async def my_coroutine():
return 42
coro = my_coroutine() # 不会执行,返回协程对象
# 需要await或包装成任务才能执行
1.3 任务(Task)
任务是对协程的封装,用于并发调度。创建任务后,协程会尽快被事件循环调度执行。使用asyncio.create_task()创建任务。
async def main():
task = asyncio.create_task(my_coroutine())
result = await task
print(result)
asyncio.run(main())
1.4 Future
Future是更低级的对象,表示一个异步操作的最终结果。任务继承自Future。通常我们不需要直接操作Future,但理解它有助于深入理解asyncio。
2. 实战:编写异步HTTP客户端
让我们用asyncio和aiohttp编写一个并发下载多个网页的示例。
首先安装aiohttp:
pip install aiohttp
然后编写代码:
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://httpbin.org/get',
'https://httpbin.org/ip',
'https://httpbin.org/user-agent'
]
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(fetch(session, url)) for url in urls]
results = await asyncio.gather(*tasks)
for result in results:
print(result[:100])
asyncio.run(main())
这里我们使用了asyncio.gather()来并发运行多个任务,它会在所有任务完成后返回结果列表。
3. 控制并发:信号量与限制
直接创建大量任务可能导致资源耗尽。我们可以使用asyncio.Semaphore来限制并发数。
import asyncio
import aiohttp
async def fetch(session, url, semaphore):
async with semaphore:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [f'https://httpbin.org/get?i={i}' for i in range(100)]
semaphore = asyncio.Semaphore(10) # 最多同时10个请求
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(fetch(session, url, semaphore)) for url in urls]
results = await asyncio.gather(*tasks)
print(f'下载了{len(results)}个页面')
asyncio.run(main())
4. 超时控制与取消任务
异步操作可能永远不返回,所以超时控制至关重要。asyncio.wait_for可以设置超时。
import asyncio
async def slow_operation():
await asyncio.sleep(10)
return 'done'
async def main():
try:
result = await asyncio.wait_for(slow_operation(), timeout=2)
print(result)
except asyncio.TimeoutError:
print('操作超时')
asyncio.run(main())
任务取消:我们可以调用task.cancel()来取消任务。
async def main():
task = asyncio.create_task(slow_operation())
await asyncio.sleep(1)
task.cancel()
try:
await task
except asyncio.CancelledError:
print('任务被取消')
5. 常见陷阱与最佳实践
陷阱1:在异步代码中调用同步阻塞函数
如果在协程中调用time.sleep()或requests.get(),会阻塞整个事件循环。
错误示例:
import time
import asyncio
async def bad_coroutine():
time.sleep(1) # 阻塞!
正确做法:使用asyncio.sleep()或await loop.runinexecutor()。
async def good_coroutine():
await asyncio.sleep(1) # 非阻塞
# 对于必须的同步阻塞函数,使用线程池
import asyncio
import time
async def main():
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, time.sleep, 1) # 在线程池中执行
陷阱2:忘记await协程
如果忘记await,协程不会执行,只会创建一个协程对象。这常导致任务未运行。
async def main():
coro = my_coroutine() # 没有await
print('协程未执行')
陷阱3:在事件循环外创建任务
在事件循环外调用asyncio.create_task()会报错。确保在协程内部调用。
最佳实践:使用asyncio.run()统一管理循环
对于简单脚本,使用asyncio.run()即可。但对于更复杂的应用,可能需要手动管理循环。
async def main():
# 主逻辑
pass
if __name__ == '__main__':
asyncio.run(main())
最佳实践:优雅关闭资源
使用async with管理资源,确保清理。
async def main():
async with aiohttp.ClientSession() as session:
# 使用session
pass
6. 总结与延伸
本文介绍了asyncio的核心概念,并通过实战展示了如何编写并发HTTP客户端、控制并发数、处理超时和取消。同时,我们指出了常见的陷阱和最佳实践。
要深入学习异步编程,可以进一步探索以下主题:
asyncio.Queue用于生产者-消费者模式- 异步上下文管理器和异步迭代器
- 使用
asyncio.createsubprocessshell执行子进程 - 与多线程/多进程的组合使用
异步编程是现代Python开发的重要技能,掌握它能让你写出更高效、更可扩展的应用程序。希望本文能为你打下坚实的基础。