1. 为什么我们需要异步编程?
在传统的同步编程模型中,代码按照顺序一行一行执行,当遇到I/O操作(如网络请求、文件读写)时,整个程序会被阻塞,直到操作完成。这种模式在处理高并发场景时效率极低,因为CPU大部分时间都在等待I/O操作完成。
举个例子,假设我们要爬取100个网页:
python复制import requests
def fetch(url):
response = requests.get(url)
return response.text
urls = ['http://example.com/1', 'http://example.com/2', ...] # 100个URL
for url in urls:
content = fetch(url) # 每个请求都会阻塞程序
process(content)
这种同步方式下,假设每个请求耗时1秒,100个请求就需要100秒。但实际上,大部分时间都浪费在等待网络响应上,CPU利用率很低。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Asyncio核心概念解析
2.1 事件循环(Event Loop)
事件循环是asyncio的核心,它负责调度和执行协程。你可以把它想象成一个无限循环,不断检查哪些协程可以运行,哪些需要等待I/O。
创建和运行事件循环的基本方式:
python复制import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
asyncio.run(main()) # Python 3.7+推荐方式
2.2 协程(Coroutine)
协程是asyncio的基本执行单元,通过async/await语法定义和使用。与普通函数不同,协程遇到await表达式时会暂停执行,让出控制权给事件循环。
定义协程的几种方式:
python复制# 方式1:async def
async def fetch_data():
return "data"
# 方式2:@asyncio.coroutine装饰器(Python 3.4-3.7)
@asyncio.coroutine
def old_style_coro():
yield from asyncio.sleep(1)
2.3 Future和Task
Future代表一个异步操作的最终结果,而Task是Future的子类,用于包装和管理协程的执行。我们通常不需要直接操作Future,而是使用Task。
创建任务的几种方式:
python复制async def my_coro():
await asyncio.sleep(1)
return 42
# 方式1:asyncio.create_task() (Python 3.7+)
task = asyncio.create_task(my_coro())
# 方式2:ensure_future (兼容旧版本)
task = asyncio.ensure_future(my_coro())
3. 实战:构建异步网络爬虫
让我们用asyncio实现一个高效的网页爬虫。我们将使用aiohttp库来处理HTTP请求,因为它提供了异步的HTTP客户端。
3.1 安装依赖
bash复制pip install aiohttp
3.2 基础爬虫实现
python复制import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://example.org',
'https://example.net'
]
async with aiohttp.ClientSession() as session:
tasks = []
for url in urls:
task = asyncio.create_task(fetch(session, url))
tasks.append(task)
pages = await asyncio.gather(*tasks)
for page in pages:
print(f"Got page with length: {len(page)}")
asyncio.run(main())
3.3 添加错误处理和限速
在实际应用中,我们需要考虑错误处理和请求限速:
python复制async def fetch_with_retry(session, url, max_retries=3):
for attempt in range(max_retries):
try:
async with session.get(url, timeout=5) as response:
response.raise_for_status()
return await response.text()
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt == max_retries - 1:
raise
await asyncio.sleep(1 * (attempt + 1))
async def main():
urls = [...] # 大量URL
# 限制并发数为10
semaphore = asyncio.Semaphore(10)
async def limited_fetch(session, url):
async with semaphore:
return await fetch_with_retry(session, url)
async with aiohttp.ClientSession() as session:
tasks = [limited_fetch(session, url) for url in urls]
pages = await asyncio.gather(*tasks, return_exceptions=True)
for page in pages:
if isinstance(page, Exception):
print(f"Error fetching page: {page}")
else:
process_page(page)
4. 常见陷阱与性能优化
4.1 阻塞操作问题
在协程中执行阻塞操作(如time.sleep()、CPU密集型计算)会破坏异步模型的优势。解决方案:
- 使用asyncio.sleep()替代time.sleep()
- 将CPU密集型任务放到线程池中执行:
python复制async def cpu_bound():
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, cpu_intensive_function)
4.2 协程未await
忘记await协程是常见错误:
python复制async def my_coro():
await asyncio.sleep(1)
# 错误:没有await
my_coro() # 这不会执行协程
# 正确
await my_coro()
4.3 任务取消处理
正确处理任务取消:
python复制async def long_running_task():
try:
await asyncio.sleep(3600)
except asyncio.CancelledError:
print("Task was cancelled")
raise # 重新抛出以标记任务为已取消
async def main():
task = asyncio.create_task(long_running_task())
await asyncio.sleep(1)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("Main caught cancellation")
4.4 性能优化技巧
- 合理设置并发限制(使用Semaphore)
- 复用ClientSession(而不是为每个请求创建新session)
- 使用连接池
- 考虑使用uvloop替代默认事件循环(性能提升2-4倍):
bash复制pip install uvloop
python复制import uvloop
uvloop.install() # 放在asyncio导入前
5. Asyncio与其他并发模型的对比
5.1 多线程 vs Asyncio
| 特性 | 多线程 | Asyncio |
|---|---|---|
| 并发模型 | 抢占式调度 | 协作式调度 |
| 上下文切换 | 操作系统控制,开销较大 | 用户空间控制,开销小 |
| 内存使用 | 每个线程需要独立栈 | 所有协程共享一个线程 |
| 适用场景 | CPU密集型任务 | I/O密集型任务 |
| 调试难度 | 较难(竞态条件) | 相对容易 |
5.2 多进程 vs Asyncio
多进程适合CPU密集型任务,可以充分利用多核CPU。Asyncio适合I/O密集型任务,在单线程内实现高并发。
实际项目中,可以结合使用:
python复制import concurrent.futures
async def mixed_workload():
loop = asyncio.get_event_loop()
# CPU密集型任务放到进程池
with concurrent.futures.ProcessPoolExecutor() as pool:
cpu_result = await loop.run_in_executor(pool, cpu_intensive_func)
# I/O密集型任务用asyncio处理
io_result = await io_bound_operation()
return cpu_result, io_result
6. 高级应用场景
6.1 WebSocket客户端
python复制import aiohttp
import asyncio
async def websocket_client():
async with aiohttp.ClientSession() as session:
async with session.ws_connect('wss://echo.websocket.org') as ws:
await ws.send_str('Hello World!')
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
print(f"Received: {msg.data}")
elif msg.type == aiohttp.WSMsgType.CLOSED:
break
elif msg.type == aiohttp.WSMsgType.ERROR:
break
asyncio.run(websocket_client())
6.2 定时任务
python复制async def periodic(interval_sec):
while True:
print("Doing periodic work")
await asyncio.sleep(interval_sec)
async def main():
task = asyncio.create_task(periodic(5))
await asyncio.sleep(20) # 让定时任务运行一段时间
task.cancel()
try:
await task
except asyncio.CancelledError:
print("Periodic task cancelled")
asyncio.run(main())
6.3 多协程协同工作
使用Queue实现生产者-消费者模式:
python复制async def producer(queue, n):
for x in range(n):
await queue.put(x)
await asyncio.sleep(0.1)
await queue.put(None) # 结束信号
async def consumer(queue):
while True:
item = await queue.get()
if item is None:
break
print(f"Consumed: {item}")
async def main():
queue = asyncio.Queue()
await asyncio.gather(
producer(queue, 10),
consumer(queue)
)
asyncio.run(main())
7. 调试与测试异步代码
7.1 调试技巧
- 使用
asyncio.debug模式:
python复制asyncio.run(main(), debug=True)
- 检查未await的协程:
python复制import warnings
warnings.simplefilter('always', RuntimeWarning)
- 使用
asyncio.all_tasks()查看所有运行中的任务
7.2 单元测试
使用pytest-asyncio插件测试异步代码:
bash复制pip install pytest-asyncio
测试示例:
python复制import pytest
@pytest.mark.asyncio
async def test_fetch():
async with aiohttp.ClientSession() as session:
content = await fetch(session, 'https://example.com')
assert 'Example Domain' in content
7.3 性能分析
使用cProfile分析异步代码:
python复制import cProfile
import asyncio
async def my_coro():
await asyncio.sleep(1)
def main():
asyncio.run(my_coro())
cProfile.run('main()', sort='cumtime')
8. 实际项目中的最佳实践
- 结构化项目布局:
code复制my_async_project/
├── __init__.py
├── main.py # 入口点
├── core/ # 核心逻辑
│ ├── __init__.py
│ ├── fetcher.py # 网络请求相关
│ └── parser.py # 数据处理
├── utils/ # 工具函数
│ ├── __init__.py
│ ├── logger.py
│ └── decorators.py
└── tests/ # 测试代码
- 配置管理:
python复制# config.py
class Config:
MAX_CONCURRENT = 10
TIMEOUT = 30
RETRIES = 3
# 使用
from config import Config
async def fetch(session, url):
async with session.get(url, timeout=Config.TIMEOUT) as response:
return await response.text()
- 日志记录:
python复制import logging
logger = logging.getLogger(__name__)
async def fetch(session, url):
try:
logger.debug(f"Fetching {url}")
async with session.get(url) as response:
return await response.text()
except Exception as e:
logger.error(f"Error fetching {url}: {e}")
raise
- 优雅关闭:
python复制async def shutdown(signal, loop):
"""Cleanup tasks tied to the service's shutdown."""
logging.info(f"Received exit signal {signal.name}...")
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
[task.cancel() for task in tasks]
logging.info(f"Cancelling {len(tasks)} outstanding tasks")
await asyncio.gather(*tasks, return_exceptions=True)
loop.stop()
async def main():
loop = asyncio.get_running_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(
sig,
lambda s=sig: asyncio.create_task(shutdown(s, loop))
)
# 主应用逻辑
try:
while True:
await asyncio.sleep(1)
except asyncio.CancelledError:
pass
finally:
logging.info("Successfully shutdown the service")
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
