1. 为什么我们需要异步编程?
在传统的同步编程模型中,代码按照顺序逐行执行,当遇到I/O操作(如网络请求、文件读写)时,整个程序会阻塞等待操作完成。这种阻塞式编程在单线程环境下尤其低效,因为CPU在等待I/O时处于闲置状态。
举个例子,假设我们要从三个不同的API获取数据:
python复制import requests
def fetch_data_sync():
data1 = requests.get('https://api1.example.com').json() # 阻塞1秒
data2 = requests.get('https://api2.example.com').json() # 阻塞1秒
data3 = requests.get('https://api3.example.com').json() # 阻塞1秒
return [data1, data2, data3]
这段代码总耗时至少3秒,因为每个请求都是顺序执行的。而异步编程可以让这三个请求并发执行,理论上总耗时只需1秒左右。
关键区别:同步编程是"做一件事等它完成再做下一件",异步编程是"开始一件事后立即去做下一件,等通知再回来处理结果"
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Python异步编程的核心概念
2.1 事件循环(Event Loop)
事件循环是异步编程的引擎,它不断检查并执行以下任务:
- 从任务队列获取待执行任务
- 执行任务直到遇到await
- 挂起当前任务,执行队列中的下一个任务
- 当await的操作完成时,恢复被挂起的任务
用餐厅比喻:
- 服务员(事件循环)同时服务多桌客人(任务)
- 当某桌需要等菜(await)时,服务员去服务其他桌
- 菜好了(await完成),服务员回来继续服务这桌
2.2 协程(Coroutine)
协程是可暂停和恢复的函数,通过async def定义:
python复制async def fetch_data():
# 模拟I/O操作
await asyncio.sleep(1)
return "data"
关键特点:
- 调用协程不会立即执行,而是返回一个协程对象
- 必须用await或run来实际执行
- 一个协程中可以await其他协程
2.3 Future和Task
- Future:表示异步操作的最终结果,是底层对象
- Task:Future的子类,包装协程以便调度
创建任务的两种方式:
python复制task = asyncio.create_task(fetch_data()) # Python 3.7+
task = asyncio.ensure_future(fetch_data()) # 旧版本兼容
3. 第一个完整异步程序
让我们实现一个并发获取多个URL的示例:
python复制import asyncio
import aiohttp # 需要安装:pip install aiohttp
async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://example.org',
'https://example.net'
]
# 创建任务列表
tasks = [fetch_url(url) for url in urls]
# 并发执行
results = await asyncio.gather(*tasks)
for url, content in zip(urls, results):
print(f"{url}: {len(content)} bytes")
# Python 3.7+ 运行方式
asyncio.run(main())
关键点解析:
aiohttp是异步HTTP客户端,替代requestsasyncio.gather用于并发运行多个协程asyncio.run是Python 3.7+推荐的运行入口
4. 异步编程的常见陷阱与解决方案
4.1 阻塞代码破坏事件循环
错误示例:
python复制async def bad_example():
await asyncio.sleep(1)
time.sleep(1) # 同步阻塞调用!
解决方案:
- 将同步I/O替换为异步版本(如aiofiles替代文件操作)
- 实在无法避免时,使用
loop.run_in_executor:
python复制async def good_example():
await asyncio.sleep(1)
await loop.run_in_executor(None, time.sleep, 1) # 在线程池中运行
4.2 任务未正确等待
错误示例:
python复制async def main():
asyncio.create_task(fetch_data()) # 任务被创建但未等待
print("Done") # 可能先于任务完成执行
正确做法:
python复制async def main():
task = asyncio.create_task(fetch_data())
await task # 明确等待任务完成
print("Done")
4.3 回调地狱(Callback Hell)
虽然asyncio支持回调,但过度使用会导致代码难以维护:
python复制# 不推荐的做法
def callback1(result):
def callback2(result2):
def callback3(result3):
...
future3.add_done_callback(callback3)
future2.add_done_callback(callback2)
future1.add_done_callback(callback1)
推荐使用async/await的线性写法:
python复制async def better_approach():
result1 = await op1()
result2 = await op2(result1)
result3 = await op3(result2)
...
5. 性能对比:同步 vs 异步
我们通过一个基准测试来直观感受性能差异:
python复制import asyncio
import time
import aiohttp
import requests
async def async_fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
def sync_fetch(url):
return requests.get(url).text
async def async_main():
urls = ["https://example.com"] * 10
tasks = [async_fetch(url) for url in urls]
await asyncio.gather(*tasks)
def sync_main():
urls = ["https://example.com"] * 10
for url in urls:
sync_fetch(url)
# 测试异步版本
start = time.time()
asyncio.run(async_main())
async_duration = time.time() - start
# 测试同步版本
start = time.time()
sync_main()
sync_duration = time.time() - start
print(f"异步耗时: {async_duration:.2f}s")
print(f"同步耗时: {sync_duration:.2f}s")
print(f"性能提升: {sync_duration/async_duration:.1f}x")
典型输出结果:
code复制异步耗时: 1.23s
同步耗时: 10.56s
性能提升: 8.6x
6. 何时使用异步编程
6.1 理想场景
- I/O密集型应用(网络请求、数据库访问)
- 高并发服务(Web服务器、API网关)
- 需要大量并行连接的应用(爬虫、监控系统)
6.2 不适用场景
- CPU密集型任务(异步不会提升计算性能)
- 简单脚本(同步代码更直观)
- 已有成熟同步解决方案且性能足够
6.3 混合场景处理
对于既有CPU计算又有I/O的场景,可以采用:
python复制async def mixed_work():
# I/O部分
data = await fetch_data()
# CPU密集型部分
processed = await loop.run_in_executor(
None,
cpu_intensive_function,
data
)
# 继续异步处理
await store_result(processed)
7. 调试异步代码的技巧
7.1 查看当前任务
python复制async def debug_example():
task = asyncio.current_task()
print(f"Task name: {task.get_name()}")
print(f"Running in: {task.get_coro()}")
7.2 设置超时
python复制async def fetch_with_timeout():
try:
async with asyncio.timeout(3.0): # Python 3.11+
await fetch_data()
except TimeoutError:
print("请求超时")
7.3 日志记录
配置异步日志:
python复制import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
async def logged_task():
logger.info("任务开始")
await asyncio.sleep(1)
logger.info("任务完成")
8. 实际项目结构建议
对于大型异步项目,推荐的组织方式:
code复制project/
├── main.py # 入口文件
├── core/ # 核心逻辑
│ ├── __init__.py
│ ├── api.py # 异步API客户端
│ └── db.py # 异步数据库访问
├── utils/ # 工具函数
│ ├── __init__.py
│ └── async_tools.py
└── config.py # 配置
关键实践:
- 将异步代码与同步代码明确分离
- 为每个模块定义清晰的接口
- 使用类型注解提高可维护性
python复制# core/api.py 示例
from typing import AsyncGenerator
import aiohttp
class APIClient:
def __init__(self, base_url: str):
self.base_url = base_url
async def fetch_items(self) -> AsyncGenerator[dict, None]:
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.base_url}/items") as resp:
data = await resp.json()
for item in data['items']:
yield item
9. 进阶学习路径
掌握基础后,可以继续学习:
- 异步上下文管理器:
python复制class AsyncResource:
async def __aenter__(self):
await self.connect()
return self
async def __aexit__(self, *exc):
await self.close()
- 异步迭代器:
python复制class AsyncCounter:
def __init__(self, stop):
self.current = 0
self.stop = stop
def __aiter__(self):
return self
async def __anext__(self):
if self.current >= self.stop:
raise StopAsyncIteration
await asyncio.sleep(0.1)
self.current += 1
return self.current - 1
- 高级模式:
- 信号量控制并发度
- 异步队列实现生产者-消费者
- 使用uvloop提升性能(替代默认事件循环)
python复制# 信号量示例
sem = asyncio.Semaphore(5) # 最大5个并发
async def limited_task():
async with sem:
await fetch_data()
10. 资源推荐
-
官方文档:
-
实用库:
- HTTP客户端:aiohttp
- 数据库:asyncpg (PostgreSQL)
- 测试:pytest-asyncio
-
调试工具:
- aiomonitor - 运行时监控
- yappi - 异步性能分析
-
最佳实践:
- 总是为任务设置名称(
asyncio.create_task(..., name="task1")) - 使用
asyncio.run()而非手动管理事件循环 - 避免在异步代码中使用全局状态
- 总是为任务设置名称(
我在实际项目中最常遇到的异步编程问题是忘记await调用协程函数,这会导致协程没有被实际执行。一个有用的检查方法是使用静态类型检查工具如mypy,它能捕获这类错误:
python复制async def foo():
return 42
async def bad():
foo() # mypy会报错: "foo"被调用但未await
async def good():
await foo() # 正确
