在IO密集型任务(网络请求、文件读写、数据库查询)面前,Python的同步模型常常因为阻塞等待而浪费大量CPU时间。异步编程通过事件循环和协程,让单线程也能并发处理成百上千个任务,显著提升吞吐量。本文从零开始,结合代码讲解Python的asyncio库,带你写出真正的异步程序。
- 同步 vs 异步:一个直观对比
假设我们要模拟三个独立的IO请求,每个耗时1秒。同步版本会线性执行,总共需要3秒。异步版本可以在等待一个请求时切换到另一个,总时间接近1秒(不考虑调度开销)。
同步代码:
```python
import time
def io_task(name, seconds):
print(f'[{name}] 开始')
time.sleep(seconds) # 阻塞整个线程
print(f'[{name}] 结束')
return name
start = time.time()
results = [io_task(f'task{i}', 1) for i in range(3)]
print(f'总耗时: {time.time() - start:.2f}s')
```
输出:
```
task0\] 开始 \[task0\] 结束 \[task1\] 开始 \[task1\] 结束 \[task2\] 开始 \[task2\] 结束 总耗时: 3.00s \`\`\` 异步版本(使用asyncio): \`\`\`python import asyncio import time async def io_task(name, seconds): print(f'\[{name}\] 开始') await asyncio.sleep(seconds) # 非阻塞等待 print(f'\[{name}\] 结束') return name async def main(): tasks = \[asyncio.create_task(io_task(f'task{i}', 1)) for i in range(3)
results = await asyncio.gather(*tasks)
print(results)
start = time.time()
asyncio.run(main())
print(f'总耗时: {time.time() - start:.2f}s')
```
输出:
```
task0\] 开始 \[task1\] 开始 \[task2\] 开始 \[task0\] 结束 \[task1\] 结束 \[task2\] 结束 \['task0', 'task1', 'task2'
总耗时: 1.00s
```
三个任务几乎同时开始,总耗时仅1秒。关键点在于await asyncio.sleep(1)让出控制权,事件循环可以执行其他任务。
- 核心概念:协程、任务与事件循环
· 协程(coroutine):由async def定义的函数。调用时不会立即执行,而是返回一个协程对象。
· 任务(Task):通过asyncio.create_task()将协程包装为任务,实现"并发"调度。任务会在事件循环中独立运行。
· 事件循环(Event Loop):核心调度器,负责管理所有任务,并在它们等待IO或sleep时切换。
下图简示事件循环的工作(文字描述版):
```
事件循环启动 -> 任务A执行 -> 遇到await -> 任务A挂起,切换至任务B -> 任务B执行... -> 挂起的任务等待条件满足后恢复
```
- 实际案例:异步HTTP请求
结合aiohttp库(需先安装:pip install aiohttp),我们实现一个高效的并发网页抓取器。
```python
import asyncio
import aiohttp
import time
async def fetch_url(session, url):
async with session.get(url) as response:
模拟解析HTML(仅获取状态码作为示例)
status = response.status
html_len = len(await response.text())
return url, status, html_len
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
print(f'请求失败: {result}')
else:
url, status, size = result
print(f'{url} -> 状态码 {status}, 内容长度 {size}')
if name == 'main':
test_urls = [
'https://www.stackoverflow.com',
'https://httpbin.org/delay/2', # 故意延迟2秒
]
start = time.time()
asyncio.run(main(test_urls))
print(f'总耗时: {time.time() - start:.2f}s')
```
输出示例:
```
https://www.python.org -> 状态码 200, 内容长度 50434
https://www.github.com -> 状态码 200, 内容长度 202022
https://httpbin.org/delay/2 -> 状态码 200, 内容长度 302
https://www.stackoverflow.com -> 状态码 200, 内容长度 728729
总耗时: 2.17s
```
即使存在一个延迟2秒的请求,总耗时也仅2秒出头,所有请求并发执行。
- 并发控制:限制同时执行的任务数
在某些场景(如API速率限制)下,我们需要控制并发数量。asyncio.Semaphore可以轻松实现。
```python
async def fetch_with_limit(sem, session, url):
async with sem: # 限制同时最多N个请求
return await fetch_url(session, url)
async def main_with_limit(urls, limit=3):
sem = asyncio.Semaphore(limit)
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_limit(sem, session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
... 处理结果
```
这样最多同时发起3个请求,其余请求会等待信号量释放。
- 错误处理与超时
5.1 单个任务的超时
使用asyncio.wait_for为协程设置超时。
```python
try:
result = await asyncio.wait_for(coro, timeout=5.0)
except asyncio.TimeoutError:
print('任务超时')
```
5.2 整体超时
asyncio.timeout上下文管理器(Python 3.11+)或老版本的asyncio.wait_for包裹整个gather。
```python
async def main_with_timeout(urls, total_timeout=10):
try:
async with asyncio.timeout(total_timeout):
await main(urls)
except asyncio.TimeoutError:
print(f'整体执行超过{total_timeout}秒')
```
5.3 捕获任务异常
gather的return_exceptions=True会将异常作为结果返回,而不是立即抛出。或者使用asyncio.Task.exception()方法。
- 进阶:同步代码与异步代码混合
如果必须调用一个阻塞的第三方库(如requests),可以用asyncio.to_thread(Python 3.9+)将其放到线程池执行,避免阻塞事件循环。
```python
import requests
async def blocking_io():
在线程池中运行阻塞的requests.get
result = await asyncio.to_thread(requests.get, 'https://api.example.com/data')
return result.json()
```
对于CPU密集型任务,应当使用multiprocessing或asyncio配合concurrent.futures.ProcessPoolExecutor。
- 常见陷阱与最佳实践
陷阱 解决方案
在协程中调用time.sleep() 必须使用await asyncio.sleep()
忘记await协程函数 调用协程函数会返回协程对象,必须await或create_task
在同步函数中直接运行异步代码 使用asyncio.run()(但只能调用一次)或创建新事件循环
大量任务时未设置并发限制 用Semaphore限流,避免压垮服务端或超出系统限制
未处理任务异常导致静默失败 为每个任务添加错误回调或使用gather(return_exceptions=True)
- 总结
· 异步编程适合IO密集型任务,能成倍提升效率。
· asyncio采用协程+事件循环模型,需要你将所有阻塞操作替换为异步版本(如aiohttp替换requests)。
· 掌握create_task、gather、wait_for、Semaphore等API,可以灵活控制并发。
· 注意混合线程池/进程池处理不可避免的同步或CPU密集型代码。
现在你可以尝试将项目中的同步IO部分重写为异步模式了。异步不是银弹,但用对场景会让你的代码飞起来。