第3章:异步编程模型:asyncio/async-http深度应用
目录
- [3.1 引言:为什么需要异步编程?](#3.1 引言:为什么需要异步编程?)
- [3.1.1 同步编程的局限性](#3.1.1 同步编程的局限性)
- [3.1.2 异步编程的优势](#3.1.2 异步编程的优势)
- [3.1.3 异步 vs 多线程 vs 多进程](#3.1.3 异步 vs 多线程 vs 多进程)
- [3.2 事件循环(Event Loop)深度解析](#3.2 事件循环(Event Loop)深度解析)
- [3.2.1 什么是事件循环?](#3.2.1 什么是事件循环?)
- [3.2.2 select/poll/epoll机制](#3.2.2 select/poll/epoll机制)
- [3.2.3 任务队列和回调队列](#3.2.3 任务队列和回调队列)
- [3.2.4 事件循环的执行流程](#3.2.4 事件循环的执行流程)
- [3.3 协程(Coroutine)执行机制](#3.3 协程(Coroutine)执行机制)
- [3.3.1 生成器的yield语法](#3.3.1 生成器的yield语法)
- [3.3.2 yield from的委托语法](#3.3.2 yield from的委托语法)
- [3.3.3 async/await的工作原理](#3.3.3 async/await的工作原理)
- [3.3.4 协程的状态机](#3.3.4 协程的状态机)
- [3.4 异步上下文管理器和迭代器](#3.4 异步上下文管理器和迭代器)
- [3.4.1 async with的实现原理](#3.4.1 async with的实现原理)
- [3.4.2 async for的实现原理](#3.4.2 async for的实现原理)
- [3.5 asyncio模块深度应用](#3.5 asyncio模块深度应用)
- [3.5.1 基础用法(create_task、gather、wait)](#3.5.1 基础用法(create_task、gather、wait))
- [3.5.2 并发原语(Lock、Semaphore、Event、Condition、Queue)](#3.5.2 并发原语(Lock、Semaphore、Event、Condition、Queue))
- [3.5.3 TaskGroup(Python 3.11+)](#3.5.3 TaskGroup(Python 3.11+))
- [3.5.4 uvloop事件循环](#3.5.4 uvloop事件循环)
- [3.6 异步HTTP客户端:aiohttp和httpx](#3.6 异步HTTP客户端:aiohttp和httpx)
- [3.6.1 aiohttp的使用](#3.6.1 aiohttp的使用)
- [3.6.2 httpx异步客户端的使用](#3.6.2 httpx异步客户端的使用)
- [3.6.3 性能对比](#3.6.3 性能对比)
- [3.7 实战演练:构建1000并发的高性能异步爬虫](#3.7 实战演练:构建1000并发的高性能异步爬虫)
- [3.7.1 步骤1:编写同步爬虫基准代码](#3.7.1 步骤1:编写同步爬虫基准代码)
- [3.7.2 步骤2:改写为异步爬虫代码](#3.7.2 步骤2:改写为异步爬虫代码)
- [3.7.3 步骤3:使用信号量控制并发数](#3.7.3 步骤3:使用信号量控制并发数)
- [3.7.4 步骤4:实现异步DNS解析和代理轮换](#3.7.4 步骤4:实现异步DNS解析和代理轮换)
- [3.7.5 步骤5:进行性能基准测试](#3.7.5 步骤5:进行性能基准测试)
- [3.7.6 步骤6:分析和对比不同方案的性能数据](#3.7.6 步骤6:分析和对比不同方案的性能数据)
- [3.8 常见坑点与排错](#3.8 常见坑点与排错)
- [3.8.1 忘记await导致协程未执行](#3.8.1 忘记await导致协程未执行)
- [3.8.2 在协程中使用阻塞操作](#3.8.2 在协程中使用阻塞操作)
- [3.8.3 异步上下文中使用同步库](#3.8.3 异步上下文中使用同步库)
- [3.8.4 事件循环的关闭和清理](#3.8.4 事件循环的关闭和清理)
- [3.9 总结](#3.9 总结)
3.1 引言:为什么需要异步编程?
在爬虫开发中,性能往往是关键因素。传统的同步编程在处理大量I/O操作时效率低下,而异步编程可以大幅提升性能。
3.1.1 同步编程的局限性
同步爬虫的问题:
python
import requests
import time
def sync_crawler(urls):
"""同步爬虫:顺序执行"""
results = []
start_time = time.time()
for url in urls:
response = requests.get(url)
results.append(response.text)
elapsed = time.time() - start_time
print(f"同步爬虫耗时: {elapsed:.2f}秒")
return results
# 测试100个URL
urls = [f'https://httpbin.org/delay/1' for _ in range(100)]
sync_crawler(urls)
# 输出: 同步爬虫耗时: 100.00秒(每个请求1秒,100个请求)
问题分析:
- 阻塞等待:每个请求必须等待前一个请求完成
- CPU空闲:在等待网络响应时,CPU处于空闲状态
- 资源浪费:无法充分利用系统资源
时间线分析:
0 0 0 0 0 0 0 0 0 0 1 网络I/O 等待请求1完成 网络I/O 等待请求2完成 网络I/O 等待请求99完成 网络I/O 请求1 请求2 请求3 请求100 同步爬虫时间线
总耗时:100秒(每个请求1秒 × 100个请求)
3.1.2 异步编程的优势
异步爬虫的优势:
python
import asyncio
import aiohttp
import time
async def async_crawler(urls):
"""异步爬虫:并发执行"""
results = []
start_time = time.time()
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
elapsed = time.time() - start_time
print(f"异步爬虫耗时: {elapsed:.2f}秒")
return results
async def fetch_url(session, url):
"""异步获取URL"""
async with session.get(url) as response:
return await response.text()
# 测试100个URL
urls = [f'https://httpbin.org/delay/1' for _ in range(100)]
asyncio.run(async_crawler(urls))
# 输出: 异步爬虫耗时: 1.50秒(并发执行,几乎同时完成)
优势分析:
- 非阻塞等待:多个请求可以并发执行
- CPU高效利用:在等待网络响应时,可以处理其他任务
- 资源充分利用:单线程可以处理大量并发连接
时间线分析:
0 0 0 0 0 0 0 0 0 0 1 所有请求并发执行 等待所有响应完成 并发请求 异步爬虫时间线(100并发)
总耗时:约1.5秒(网络延迟 + 少量开销)
性能提升 :100秒 → 1.5秒,提升约66倍!
3.1.3 异步 vs 多线程 vs 多进程
三种并发模型的对比:
| 特性 | 异步 | 多线程 | 多进程 |
|---|---|---|---|
| 适用场景 | I/O密集型 | I/O密集型 | CPU密集型 |
| 资源消耗 | 低(单线程) | 中等(多线程) | 高(多进程) |
| 上下文切换 | 协程切换(轻量) | 线程切换(较重) | 进程切换(最重) |
| 内存占用 | 低 | 中等 | 高 |
| GIL影响 | 无(单线程) | 有(Python GIL) | 无(多进程) |
| 代码复杂度 | 中等 | 低 | 低 |
| 调试难度 | 较高 | 中等 | 较低 |
性能对比示例:
python
import asyncio
import aiohttp
import requests
import threading
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# 测试URL列表
urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
def sync_benchmark():
"""同步基准测试"""
start = time.time()
for url in urls:
requests.get(url)
return time.time() - start
def async_benchmark():
"""异步基准测试"""
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
async def run():
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
await asyncio.gather(*tasks)
start = time.time()
asyncio.run(run())
return time.time() - start
def thread_benchmark():
"""多线程基准测试"""
def fetch(url):
return requests.get(url)
start = time.time()
with ThreadPoolExecutor(max_workers=50) as executor:
executor.map(fetch, urls)
return time.time() - start
# 运行基准测试
print(f"同步: {sync_benchmark():.2f}秒")
print(f"异步: {async_benchmark():.2f}秒")
print(f"多线程: {thread_benchmark():.2f}秒")
典型结果:
- 同步:50秒
- 异步:0.8秒
- 多线程:2.5秒
结论:
- I/O密集型任务:异步 > 多线程 > 同步
- CPU密集型任务:多进程 > 多线程 > 异步
3.2 事件循环(Event Loop)深度解析
事件循环是异步编程的核心,理解事件循环的工作原理是掌握异步编程的关键。
3.2.1 什么是事件循环?
事件循环的定义:
事件循环是一个无限循环,它不断地检查是否有事件发生,并执行相应的回调函数。
简单的事件循环模型:
python
class SimpleEventLoop:
"""简单的事件循环实现"""
def __init__(self):
self.ready_queue = [] # 就绪任务队列
self.io_waiting = {} # I/O等待字典 {fd: callback}
def run_forever(self):
"""运行事件循环"""
while self.ready_queue or self.io_waiting:
# 1. 执行就绪任务
while self.ready_queue:
task = self.ready_queue.pop(0)
task()
# 2. 检查I/O事件
ready_fds = self.select_io() # 使用select/poll/epoll
for fd in ready_fds:
callback = self.io_waiting.pop(fd)
self.ready_queue.append(callback)
def select_io(self):
"""检查I/O事件(简化版)"""
# 实际使用select/poll/epoll
return []
事件循环的作用:
- 任务调度:管理协程的执行顺序
- I/O复用:监控多个I/O操作
- 回调执行:当I/O就绪时执行回调
3.2.2 select/poll/epoll机制
事件循环底层使用I/O多路复用机制来监控多个文件描述符。
select机制
工作原理:
python
import select
def select_example():
"""select机制示例"""
# 创建socket列表
sockets = [socket1, socket2, socket3]
while True:
# 检查哪些socket可读
readable, writable, exceptional = select.select(
sockets, # 可读列表
sockets, # 可写列表
sockets, # 异常列表
timeout=1.0 # 超时时间
)
for sock in readable:
data = sock.recv(1024)
# 处理数据
select的特点:
- 跨平台:Windows、Linux、macOS都支持
- 限制:最多监控1024个文件描述符(某些系统)
- 效率:O(n),需要遍历所有文件描述符
poll机制
工作原理:
python
import select
def poll_example():
"""poll机制示例"""
poller = select.poll()
# 注册文件描述符
for sock in sockets:
poller.register(sock, select.POLLIN)
while True:
# 检查事件
events = poller.poll(timeout=1000) # 超时1秒
for fd, event in events:
if event & select.POLLIN:
# 可读
data = sock.recv(1024)
poll的特点:
- 无限制:可以监控任意数量的文件描述符
- 效率:O(n),仍然需要遍历
- 平台限制:Windows不支持
epoll机制(Linux)
工作原理:
python
import select
def epoll_example():
"""epoll机制示例(仅Linux)"""
epoller = select.epoll()
# 注册文件描述符
for sock in sockets:
epoller.register(sock.fileno(), select.EPOLLIN)
while True:
# 只返回就绪的事件(高效!)
events = epoller.poll(timeout=1.0)
for fd, event in events:
if event & select.EPOLLIN:
# 可读
data = sock.recv(1024)
epoll的特点:
- 高效:O(1),只返回就绪的事件
- 平台限制:仅Linux支持
- 大量连接:适合处理大量并发连接
三种机制的性能对比
少量连接
10-100
select/poll
中等连接
100-1000
poll/epoll
大量连接
1000+
epoll
性能对比表:
| 连接数 | select | poll | epoll |
|---|---|---|---|
| 10 | 快 | 快 | 快 |
| 100 | 中等 | 中等 | 快 |
| 1000 | 慢 | 慢 | 快 |
| 10000 | 很慢 | 很慢 | 快 |
Python asyncio的选择:
- Windows :使用
select(IOCP在ProactorEventLoop中) - Linux :使用
epoll - macOS :使用
kqueue(类似epoll)
3.2.3 任务队列和回调队列
事件循环维护多个队列来管理任务和回调。
队列结构:
事件循环
就绪队列
Ready Queue
等待队列
Waiting Queue
调度队列
Scheduled Queue
立即执行
等待I/O
定时任务
队列说明:
-
就绪队列(Ready Queue):
- 存储可以立即执行的任务
- 先进先出(FIFO)
-
等待队列(Waiting Queue):
- 存储等待I/O操作的任务
- 当I/O就绪时,任务移到就绪队列
-
调度队列(Scheduled Queue):
- 存储定时任务
- 按时间排序
Python代码示例:
python
import asyncio
async def task1():
print("任务1开始")
await asyncio.sleep(1)
print("任务1结束")
async def task2():
print("任务2开始")
await asyncio.sleep(0.5)
print("任务2结束")
async def main():
# 创建任务
t1 = asyncio.create_task(task1())
t2 = asyncio.create_task(task2())
# 等待所有任务完成
await asyncio.gather(t1, t2)
# 运行
asyncio.run(main())
执行流程:
task1和task2被添加到就绪队列- 事件循环从就绪队列取出
task1执行 task1遇到await asyncio.sleep(1),被移到等待队列- 事件循环取出
task2执行 task2遇到await asyncio.sleep(0.5),被移到等待队列- 0.5秒后,
task2被移回就绪队列,继续执行 - 1秒后,
task1被移回就绪队列,继续执行
3.2.4 事件循环的执行流程
完整的事件循环执行流程:
否
是
是
否
是
否
是
否
否
是
启动事件循环
检查就绪队列
就绪队列
是否为空?
取出任务执行
检查I/O事件
任务是否
遇到await?
任务挂起
移到等待队列
任务完成
有I/O
就绪?
将等待任务
移到就绪队列
检查定时任务
有定时任务
到期?
将定时任务
移到就绪队列
事件循环
是否结束?
结束
Python代码实现:
python
import asyncio
import time
async def example_task(name, delay):
"""示例任务"""
print(f"[{time.time():.2f}] {name} 开始")
await asyncio.sleep(delay)
print(f"[{time.time():.2f}] {name} 结束")
async def main():
"""主函数"""
print("=== 事件循环执行流程演示 ===")
# 创建多个任务
tasks = [
example_task("任务1", 1.0),
example_task("任务2", 0.5),
example_task("任务3", 0.3),
]
# 并发执行
await asyncio.gather(*tasks)
# 运行
asyncio.run(main())
输出示例:
=== 事件循环执行流程演示 ===
[1234567890.00] 任务1 开始
[1234567890.00] 任务2 开始
[1234567890.00] 任务3 开始
[1234567890.30] 任务3 结束
[1234567890.50] 任务2 结束
[1234567891.00] 任务1 结束
关键点:
- 所有任务几乎同时开始
- 任务按延迟时间顺序完成
- 事件循环在等待期间可以处理其他任务
3.3 协程(Coroutine)执行机制
协程是异步编程的基本单位,理解协程的执行机制是掌握异步编程的基础。
3.3.1 生成器的yield语法
协程基于生成器实现,理解生成器有助于理解协程。
生成器基础:
python
def simple_generator():
"""简单生成器"""
print("开始")
yield 1
print("中间")
yield 2
print("结束")
yield 3
# 使用生成器
gen = simple_generator()
print(next(gen)) # 输出: 开始, 1
print(next(gen)) # 输出: 中间, 2
print(next(gen)) # 输出: 结束, 3
生成器的状态:
python
def generator_with_state():
"""带状态的生成器"""
value = 0
while value < 3:
value += 1
received = yield value
print(f"收到: {received}")
gen = generator_with_state()
print(next(gen)) # 输出: 1(首次调用需要next,不能send)
print(gen.send(10)) # 输出: 收到: 10, 2
print(gen.send(20)) # 输出: 收到: 20, 3
生成器的优势:
- 惰性计算:按需生成值
- 状态保持:可以暂停和恢复
- 内存高效:不需要存储所有值
3.3.2 yield from的委托语法
yield from允许生成器委托给另一个生成器。
基础用法:
python
def generator1():
yield 1
yield 2
def generator2():
yield from generator1() # 委托给generator1
yield 3
# 使用
for value in generator2():
print(value)
# 输出: 1, 2, 3
为什么需要yield from?
python
# 不使用yield from(繁琐)
def generator_manual():
for value in generator1():
yield value
yield 3
# 使用yield from(简洁)
def generator_delegated():
yield from generator1()
yield 3
yield from的作用:
- 简化代码:不需要手动迭代
- 值传递:自动传递send的值
- 异常处理:自动传播异常
3.3.3 async/await的工作原理
async/await是Python 3.5+引入的语法糖,基于生成器实现。
async函数本质:
python
# async函数实际上是生成器函数
async def async_function():
await asyncio.sleep(1)
return "完成"
# 等价于(简化版)
def generator_function():
yield asyncio.sleep(1)
return "完成"
await的工作原理:
python
async def example():
print("开始")
result = await asyncio.sleep(1) # 挂起,等待完成
print("结束")
return result
# await的执行流程:
# 1. 遇到await,协程挂起
# 2. 控制权返回给事件循环
# 3. 事件循环执行其他任务
# 4. await的对象完成后,协程恢复执行
await的底层实现(简化版):
python
def await_implementation(coro, value):
"""await的简化实现"""
try:
# 发送值给协程
result = coro.send(value)
# 如果返回的是另一个协程,继续await
if isinstance(result, types.CoroutineType):
return await_implementation(result, None)
return result
except StopIteration as e:
# 协程完成,返回结果
return e.value
3.3.4 协程的状态机
协程有多个状态,理解状态转换有助于调试。
协程状态:
创建协程
开始执行
遇到await
await完成
执行完成
异常或取消
CORO_CREATED
CORO_RUNNING
CORO_SUSPENDED
CORO_CLOSED
Python代码演示:
python
import asyncio
import inspect
async def example_coroutine():
print("协程开始")
print(f"状态: {inspect.getgeneratorstate(example_coroutine())}")
await asyncio.sleep(1)
print("协程恢复")
return "完成"
# 创建协程对象
coro = example_coroutine()
print(f"创建后状态: {inspect.getgeneratorstate(coro)}")
# 运行协程
result = asyncio.run(coro)
print(f"结果: {result}")
状态说明:
- CORO_CREATED:协程已创建,但未开始执行
- CORO_RUNNING:协程正在执行
- CORO_SUSPENDED:协程已挂起(遇到await)
- CORO_CLOSED:协程已关闭(执行完成或异常)
3.4 异步上下文管理器和迭代器
3.4.1 async with的实现原理
async with用于异步上下文管理,类似于同步的with语句。
基础用法:
python
async def example():
async with aiohttp.ClientSession() as session:
async with session.get('https://example.com') as response:
return await response.text()
实现原理:
python
class AsyncContextManager:
"""异步上下文管理器示例"""
async def __aenter__(self):
"""进入上下文"""
print("进入上下文")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""退出上下文"""
print("退出上下文")
return False # 不抑制异常
# 使用
async def use_context():
async with AsyncContextManager() as ctx:
print("在上下文中")
asyncio.run(use_context())
async with的执行流程:
渲染错误: Mermaid 渲染失败: Parse error on line 7: ...aenter__() CM->>Loop: await (可能挂起) ----------------------^ Expecting '+', '-', 'ACTOR', got 'loop'
3.4.2 async for的实现原理
async for用于异步迭代,类似于同步的for循环。
基础用法:
python
async def example():
async for item in async_generator():
print(item)
实现原理:
python
class AsyncIterator:
"""异步迭代器示例"""
def __init__(self, items):
self.items = items
self.index = 0
def __aiter__(self):
"""返回异步迭代器"""
return self
async def __anext__(self):
"""获取下一个元素"""
if self.index >= len(self.items):
raise StopAsyncIteration
# 模拟异步操作
await asyncio.sleep(0.1)
item = self.items[self.index]
self.index += 1
return item
# 使用
async def use_iterator():
async_iter = AsyncIterator([1, 2, 3, 4, 5])
async for item in async_iter:
print(item)
asyncio.run(use_iterator())
async for的执行流程:
渲染错误: Mermaid 渲染失败: Parse error on line 11: ...xt__() AI->>Loop: await (可能挂起) ----------------------^ Expecting '+', '-', 'ACTOR', got 'loop'
3.5 asyncio模块深度应用
3.5.1 基础用法(create_task、gather、wait)
create_task
create_task用于创建任务并立即调度执行。
python
import asyncio
async def task(name, delay):
print(f"{name} 开始")
await asyncio.sleep(delay)
print(f"{name} 结束")
return f"{name} 完成"
async def main():
# 创建任务
task1 = asyncio.create_task(task("任务1", 1.0))
task2 = asyncio.create_task(task("任务2", 0.5))
# 等待任务完成
result1 = await task1
result2 = await task2
print(f"结果: {result1}, {result2}")
asyncio.run(main())
create_task vs 直接await:
python
# 错误:顺序执行
async def wrong_way():
await task("任务1", 1.0) # 等待1秒
await task("任务2", 0.5) # 再等待0.5秒
# 总耗时: 1.5秒
# 正确:并发执行
async def right_way():
task1 = asyncio.create_task(task("任务1", 1.0))
task2 = asyncio.create_task(task("任务2", 0.5))
await task1
await task2
# 总耗时: 1.0秒(并发执行)
gather
gather用于并发执行多个协程,并收集结果。
python
async def main():
# 并发执行多个任务
results = await asyncio.gather(
task("任务1", 1.0),
task("任务2", 0.5),
task("任务3", 0.3),
)
print(f"结果: {results}")
# 输出: ['任务1 完成', '任务2 完成', '任务3 完成']
asyncio.run(main())
gather的高级用法:
python
async def main():
# return_exceptions=True: 异常不中断,返回异常对象
results = await asyncio.gather(
task("任务1", 1.0),
task("任务2", 0.5),
task("任务3", 0.3),
return_exceptions=True
)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"任务{i+1}失败: {result}")
else:
print(f"任务{i+1}成功: {result}")
wait
wait用于等待多个协程完成,提供更灵活的控制。
python
async def main():
tasks = [
asyncio.create_task(task("任务1", 1.0)),
asyncio.create_task(task("任务2", 0.5)),
asyncio.create_task(task("任务3", 0.3)),
]
# 等待所有任务完成
done, pending = await asyncio.wait(tasks)
print(f"完成: {len(done)}, 等待: {len(pending)}")
# 获取结果
for task in done:
print(f"结果: {task.result()}")
asyncio.run(main())
wait的选项:
python
# return_when参数
done, pending = await asyncio.wait(
tasks,
return_when=asyncio.FIRST_COMPLETED # 第一个完成就返回
)
# 选项:
# - asyncio.ALL_COMPLETED: 所有完成(默认)
# - asyncio.FIRST_COMPLETED: 第一个完成
# - asyncio.FIRST_EXCEPTION: 第一个异常
3.5.2 并发原语(Lock、Semaphore、Event、Condition、Queue)
Lock(锁)
用于保护共享资源,防止并发访问。
python
import asyncio
# 共享资源
counter = 0
lock = asyncio.Lock()
async def increment(name):
global counter
async with lock: # 获取锁
# 临界区
old_value = counter
await asyncio.sleep(0.1) # 模拟I/O操作
counter = old_value + 1
print(f"{name}: counter = {counter}")
async def main():
tasks = [increment(f"任务{i}") for i in range(5)]
await asyncio.gather(*tasks)
print(f"最终counter: {counter}")
asyncio.run(main())
Semaphore(信号量)
用于限制并发数量。
python
import asyncio
# 限制最多3个并发
semaphore = asyncio.Semaphore(3)
async def limited_task(name):
async with semaphore: # 获取信号量
print(f"{name} 开始")
await asyncio.sleep(1)
print(f"{name} 结束")
async def main():
tasks = [limited_task(f"任务{i}") for i in range(10)]
await asyncio.gather(*tasks)
asyncio.run(main())
执行流程:
0 0 0 0 0 0 0 0 0 0 1 任务0 任务1 任务2 任务3 任务4 任务5 任务6 任务7 任务8 任务9 并发组1 并发组2 并发组3 并发组4 信号量控制并发(最多3个)
Event(事件)
用于协程之间的通信。
python
import asyncio
event = asyncio.Event()
async def waiter(name):
print(f"{name} 等待事件")
await event.wait() # 等待事件
print(f"{name} 收到事件")
async def setter():
await asyncio.sleep(2)
print("设置事件")
event.set() # 设置事件,唤醒所有等待者
async def main():
await asyncio.gather(
waiter("等待者1"),
waiter("等待者2"),
setter()
)
asyncio.run(main())
Condition(条件变量)
用于复杂的同步场景。
python
import asyncio
condition = asyncio.Condition()
items = []
async def producer():
async with condition:
items.append("item")
print("生产者: 添加item")
condition.notify_all() # 通知所有等待者
async def consumer(name):
async with condition:
while not items:
await condition.wait() # 等待条件满足
item = items.pop()
print(f"{name}: 消费 {item}")
async def main():
await asyncio.gather(
consumer("消费者1"),
consumer("消费者2"),
asyncio.sleep(1),
producer()
)
asyncio.run(main())
Queue(队列)
用于协程之间的数据传递。
python
import asyncio
queue = asyncio.Queue(maxsize=3)
async def producer():
for i in range(5):
await queue.put(f"item{i}")
print(f"生产者: 放入 item{i}")
await asyncio.sleep(0.5)
async def consumer(name):
while True:
item = await queue.get()
print(f"{name}: 消费 {item}")
queue.task_done()
if item == "item4":
break
async def main():
await asyncio.gather(
producer(),
consumer("消费者1"),
consumer("消费者2")
)
asyncio.run(main())
3.5.3 TaskGroup(Python 3.11+)
TaskGroup是Python 3.11+引入的新特性,提供更好的任务管理。
基础用法:
python
async def main():
async with asyncio.TaskGroup() as tg:
# 创建任务
task1 = tg.create_task(task("任务1", 1.0))
task2 = tg.create_task(task("任务2", 0.5))
task3 = tg.create_task(task("任务3", 0.3))
# 所有任务完成后才退出上下文
print("所有任务完成")
asyncio.run(main())
TaskGroup的优势:
- 自动等待:退出上下文时自动等待所有任务
- 异常处理:任何任务异常会取消其他任务
- 代码简洁:不需要手动gather
对比gather:
python
# 使用gather(旧方式)
async def old_way():
try:
await asyncio.gather(
task("任务1", 1.0),
task("任务2", 0.5),
task("任务3", 0.3),
)
except Exception as e:
# 需要手动处理异常
print(f"错误: {e}")
# 使用TaskGroup(新方式)
async def new_way():
async with asyncio.TaskGroup() as tg:
tg.create_task(task("任务1", 1.0))
tg.create_task(task("任务2", 0.5))
tg.create_task(task("任务3", 0.3))
# 自动处理异常和清理
3.5.4 uvloop事件循环
uvloop是asyncio的高性能替代实现,基于libuv。
安装:
bash
pip install uvloop
使用:
python
import uvloop
import asyncio
# 设置uvloop为事件循环策略
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
async def main():
# 使用uvloop运行
await asyncio.sleep(1)
print("使用uvloop")
asyncio.run(main())
性能对比:
| 场景 | asyncio | uvloop | 提升 |
|---|---|---|---|
| 简单I/O | 基准 | 2-4x | 2-4倍 |
| 网络I/O | 基准 | 2-4x | 2-4倍 |
| 大量连接 | 基准 | 4-6x | 4-6倍 |
注意事项:
- uvloop仅支持Linux和macOS
- Windows不支持uvloop
3.6 异步HTTP客户端:aiohttp和httpx
3.6.1 aiohttp的使用
基础用法:
python
import aiohttp
import asyncio
async def fetch_url(session, url):
"""异步获取URL"""
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [
fetch_url(session, 'https://httpbin.org/get'),
fetch_url(session, 'https://httpbin.org/post'),
]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
高级用法:
python
async def advanced_example():
"""aiohttp高级用法"""
timeout = aiohttp.ClientTimeout(total=10)
connector = aiohttp.TCPConnector(limit=100)
async with aiohttp.ClientSession(
timeout=timeout,
connector=connector,
headers={'User-Agent': 'MyBot/1.0'}
) as session:
# 并发请求
async with session.get('https://example.com') as resp:
data = await resp.json()
print(data)
asyncio.run(advanced_example())
3.6.2 httpx异步客户端的使用
基础用法:
python
import httpx
import asyncio
async def fetch_url(client, url):
"""使用httpx异步获取URL"""
response = await client.get(url)
return response.text
async def main():
async with httpx.AsyncClient() as client:
tasks = [
fetch_url(client, 'https://httpbin.org/get'),
fetch_url(client, 'https://httpbin.org/post'),
]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
httpx的优势:
- API一致性:同步和异步API相同
- HTTP/2支持:支持HTTP/2协议
- 类型提示:完整的类型提示支持
3.6.3 性能对比
python
import asyncio
import aiohttp
import httpx
import time
urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
async def aiohttp_benchmark():
"""aiohttp基准测试"""
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [session.get(url) for url in urls]
await asyncio.gather(*tasks)
return time.time() - start
async def httpx_benchmark():
"""httpx基准测试"""
start = time.time()
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
await asyncio.gather(*tasks)
return time.time() - start
# 运行基准测试
print(f"aiohttp: {asyncio.run(aiohttp_benchmark()):.2f}秒")
print(f"httpx: {asyncio.run(httpx_benchmark()):.2f}秒")
典型结果:
- aiohttp: 0.8秒
- httpx: 0.9秒
选择建议:
- aiohttp:更成熟,性能略好
- httpx:API更现代,支持HTTP/2
3.7 实战演练:构建1000并发的高性能异步爬虫
3.7.1 步骤1:编写同步爬虫基准代码
python
import requests
import time
from typing import List
def sync_crawler(urls: List[str]) -> List[str]:
"""同步爬虫基准代码"""
results = []
start_time = time.time()
for url in urls:
try:
response = requests.get(url, timeout=10)
results.append(response.text[:100]) # 只取前100字符
except Exception as e:
results.append(f"错误: {e}")
elapsed = time.time() - start_time
print(f"同步爬虫:")
print(f" 总耗时: {elapsed:.2f}秒")
print(f" 请求数: {len(urls)}")
print(f" 成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
return results
# 测试
if __name__ == '__main__':
urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
sync_crawler(urls)
3.7.2 步骤2:改写为异步爬虫代码
python
import asyncio
import aiohttp
import time
from typing import List
async def async_crawler(urls: List[str]) -> List[str]:
"""异步爬虫代码"""
results = []
start_time = time.time()
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 处理异常结果
results = [str(r) if isinstance(r, Exception) else r[:100] for r in results]
elapsed = time.time() - start_time
print(f"异步爬虫:")
print(f" 总耗时: {elapsed:.2f}秒")
print(f" 请求数: {len(urls)}")
print(f" 成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
return results
async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
"""异步获取URL"""
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
return await response.text()
except Exception as e:
return f"错误: {e}"
# 测试
if __name__ == '__main__':
urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
asyncio.run(async_crawler(urls))
3.7.3 步骤3:使用信号量控制并发数
python
import asyncio
import aiohttp
import time
from typing import List
async def async_crawler_with_semaphore(urls: List[str], max_concurrent: int = 100) -> List[str]:
"""使用信号量控制并发数的异步爬虫"""
semaphore = asyncio.Semaphore(max_concurrent)
results = []
start_time = time.time()
async def fetch_with_limit(session: aiohttp.ClientSession, url: str) -> str:
async with semaphore: # 限制并发数
return await fetch_url(session, url)
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_limit(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 处理异常结果
results = [str(r) if isinstance(r, Exception) else r[:100] for r in results]
elapsed = time.time() - start_time
print(f"异步爬虫(并发数={max_concurrent}):")
print(f" 总耗时: {elapsed:.2f}秒")
print(f" 请求数: {len(urls)}")
print(f" 成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
return results
async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
"""异步获取URL"""
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
return await response.text()
except Exception as e:
return f"错误: {e}"
# 测试
if __name__ == '__main__':
urls = [f'https://httpbin.org/delay/0.5' for _ in range(1000)]
asyncio.run(async_crawler_with_semaphore(urls, max_concurrent=100))
3.7.4 步骤4:实现异步DNS解析和代理轮换
python
import asyncio
import aiohttp
import aiodns
import time
from typing import List, Optional
import random
class AsyncCrawlerWithProxy:
"""支持代理轮换的异步爬虫"""
def __init__(self, proxies: Optional[List[str]] = None, max_concurrent: int = 100):
self.proxies = proxies or []
self.max_concurrent = max_concurrent
self.resolver = aiodns.DNSResolver()
async def resolve_dns(self, hostname: str) -> str:
"""异步DNS解析"""
try:
result = await self.resolver.gethostbyname(hostname, aiodns.AF_INET)
return result.addresses[0]
except Exception as e:
print(f"DNS解析失败 {hostname}: {e}")
return hostname
def get_proxy(self) -> Optional[str]:
"""获取随机代理"""
if self.proxies:
return random.choice(self.proxies)
return None
async def fetch_url(self, session: aiohttp.ClientSession, url: str) -> str:
"""异步获取URL(支持代理)"""
proxy = self.get_proxy()
try:
async with session.get(
url,
proxy=proxy,
timeout=aiohttp.ClientTimeout(total=10)
) as response:
return await response.text()
except Exception as e:
return f"错误: {e}"
async def crawl(self, urls: List[str]) -> List[str]:
"""爬取URL列表"""
semaphore = asyncio.Semaphore(self.max_concurrent)
results = []
start_time = time.time()
async def fetch_with_limit(session: aiohttp.ClientSession, url: str) -> str:
async with semaphore:
return await self.fetch_url(session, url)
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_limit(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 处理异常结果
results = [str(r) if isinstance(r, Exception) else r[:100] for r in results]
elapsed = time.time() - start_time
print(f"异步爬虫(代理轮换):")
print(f" 总耗时: {elapsed:.2f}秒")
print(f" 请求数: {len(urls)}")
print(f" 成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
return results
# 使用示例
async def main():
# 代理列表(示例)
proxies = [
'http://proxy1.example.com:8080',
'http://proxy2.example.com:8080',
]
crawler = AsyncCrawlerWithProxy(proxies=proxies, max_concurrent=100)
urls = [f'https://httpbin.org/delay/0.5' for _ in range(1000)]
await crawler.crawl(urls)
if __name__ == '__main__':
asyncio.run(main())
3.7.5 步骤5:进行性能基准测试
python
import asyncio
import aiohttp
import requests
import time
from concurrent.futures import ThreadPoolExecutor
from typing import List
def benchmark_sync(urls: List[str]) -> float:
"""同步基准测试"""
start = time.time()
for url in urls:
requests.get(url, timeout=10)
return time.time() - start
def benchmark_thread(urls: List[str], max_workers: int = 50) -> float:
"""多线程基准测试"""
def fetch(url):
return requests.get(url, timeout=10)
start = time.time()
with ThreadPoolExecutor(max_workers=max_workers) as executor:
executor.map(fetch, urls)
return time.time() - start
async def benchmark_async(urls: List[str], max_concurrent: int = 100) -> float:
"""异步基准测试"""
semaphore = asyncio.Semaphore(max_concurrent)
async def fetch(session, url):
async with semaphore:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
return await resp.text()
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
await asyncio.gather(*tasks, return_exceptions=True)
return time.time() - start
async def run_benchmarks():
"""运行所有基准测试"""
urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
print("=== 性能基准测试 ===")
print(f"测试URL数量: {len(urls)}")
print(f"每个URL延迟: 0.5秒")
print()
# 同步测试
print("1. 同步爬虫测试...")
sync_time = benchmark_sync(urls[:10]) # 只测试10个,避免太慢
print(f" 耗时: {sync_time:.2f}秒(10个URL)")
print(f" 预估100个URL: {sync_time * 10:.2f}秒")
print()
# 多线程测试
print("2. 多线程爬虫测试...")
thread_time = benchmark_thread(urls)
print(f" 耗时: {thread_time:.2f}秒")
print()
# 异步测试
print("3. 异步爬虫测试...")
async_time = await benchmark_async(urls)
print(f" 耗时: {async_time:.2f}秒")
print()
# 性能对比
print("=== 性能对比 ===")
print(f"同步(预估): {sync_time * 10:.2f}秒")
print(f"多线程: {thread_time:.2f}秒")
print(f"异步: {async_time:.2f}秒")
print()
print(f"异步 vs 同步: {sync_time * 10 / async_time:.1f}x 倍速")
print(f"异步 vs 多线程: {thread_time / async_time:.1f}x 倍速")
if __name__ == '__main__':
asyncio.run(run_benchmarks())
3.7.6 步骤6:分析和对比不同方案的性能数据
完整的高性能异步爬虫:
python
import asyncio
import aiohttp
import time
from typing import List, Dict, Optional
import json
class HighPerformanceAsyncCrawler:
"""高性能异步爬虫"""
def __init__(
self,
max_concurrent: int = 1000,
timeout: int = 30,
proxies: Optional[List[str]] = None,
headers: Optional[Dict[str, str]] = None
):
self.max_concurrent = max_concurrent
self.timeout = aiohttp.ClientTimeout(total=timeout)
self.proxies = proxies or []
self.headers = headers or {}
self.stats = {
'total': 0,
'success': 0,
'failed': 0,
'start_time': None,
'end_time': None,
}
async def fetch_url(
self,
session: aiohttp.ClientSession,
url: str,
semaphore: asyncio.Semaphore
) -> Dict:
"""异步获取URL"""
async with semaphore:
self.stats['total'] += 1
try:
proxy = random.choice(self.proxies) if self.proxies else None
async with session.get(
url,
proxy=proxy,
timeout=self.timeout,
headers=self.headers
) as response:
text = await response.text()
self.stats['success'] += 1
return {
'url': url,
'status': response.status,
'success': True,
'length': len(text)
}
except Exception as e:
self.stats['failed'] += 1
return {
'url': url,
'status': 0,
'success': False,
'error': str(e)
}
async def crawl(self, urls: List[str]) -> List[Dict]:
"""爬取URL列表"""
self.stats['start_time'] = time.time()
semaphore = asyncio.Semaphore(self.max_concurrent)
async with aiohttp.ClientSession() as session:
tasks = [
self.fetch_url(session, url, semaphore)
for url in urls
]
results = await asyncio.gather(*tasks, return_exceptions=True)
self.stats['end_time'] = time.time()
self.print_stats()
return results
def print_stats(self):
"""打印统计信息"""
elapsed = self.stats['end_time'] - self.stats['start_time']
qps = self.stats['total'] / elapsed if elapsed > 0 else 0
print("=== 爬虫统计 ===")
print(f"总请求数: {self.stats['total']}")
print(f"成功: {self.stats['success']}")
print(f"失败: {self.stats['failed']}")
print(f"成功率: {self.stats['success'] / self.stats['total'] * 100:.1f}%")
print(f"总耗时: {elapsed:.2f}秒")
print(f"QPS: {qps:.1f} 请求/秒")
print(f"平均延迟: {elapsed / self.stats['total'] * 1000:.1f}ms")
# 使用示例
async def main():
crawler = HighPerformanceAsyncCrawler(
max_concurrent=1000,
timeout=30,
headers={'User-Agent': 'MyBot/1.0'}
)
urls = [f'https://httpbin.org/delay/0.1' for _ in range(1000)]
results = await crawler.crawl(urls)
# 保存结果
with open('results.json', 'w') as f:
json.dump(results, f, indent=2)
if __name__ == '__main__':
asyncio.run(main())
3.8 常见坑点与排错
3.8.1 忘记await导致协程未执行
错误示例:
python
async def fetch_data():
return "数据"
async def main():
# 错误:忘记await
result = fetch_data() # 这是一个协程对象,不是结果!
print(result) # 输出: <coroutine object fetch_data at 0x...>
asyncio.run(main())
正确做法:
python
async def main():
# 正确:使用await
result = await fetch_data()
print(result) # 输出: 数据
asyncio.run(main())
Tips/坑点:
- ⚠️ 忘记await会导致协程未执行,返回协程对象而不是结果
3.8.2 在协程中使用阻塞操作
错误示例:
python
import time
async def bad_example():
# 错误:使用time.sleep会阻塞整个事件循环
time.sleep(1) # 阻塞!
return "完成"
async def main():
tasks = [bad_example() for _ in range(10)]
await asyncio.gather(*tasks)
# 总耗时: 10秒(顺序执行)
asyncio.run(main())
正确做法:
python
async def good_example():
# 正确:使用asyncio.sleep
await asyncio.sleep(1) # 非阻塞
return "完成"
async def main():
tasks = [good_example() for _ in range(10)]
await asyncio.gather(*tasks)
# 总耗时: 1秒(并发执行)
asyncio.run(main())
常见阻塞操作:
| 阻塞操作 | 异步替代 |
|---|---|
time.sleep() |
asyncio.sleep() |
requests.get() |
aiohttp.ClientSession.get() |
open() |
aiofiles.open() |
socket.recv() |
asyncio streams |
Tips/坑点:
- ⚠️ 在协程中使用阻塞操作(如time.sleep)会阻塞整个事件循环
3.8.3 异步上下文中使用同步库
错误示例:
python
async def bad_example():
# 错误:在异步上下文中使用同步库
import requests
response = requests.get('https://example.com') # 阻塞!
return response.text
正确做法:
python
async def good_example():
# 正确:使用异步库
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get('https://example.com') as response:
return await response.text()
Tips/坑点:
- ⚠️ 异步上下文中使用同步库会失去异步优势
3.8.4 事件循环的关闭和清理
错误示例:
python
# 错误:在已有事件循环中创建新的事件循环
async def bad_example():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 可能导致问题
正确做法:
python
# 正确:使用asyncio.run
async def good_example():
# 代码
pass
asyncio.run(good_example()) # 自动管理事件循环
清理资源:
python
async def example_with_cleanup():
session = aiohttp.ClientSession()
try:
# 使用session
async with session.get('https://example.com') as response:
return await response.text()
finally:
# 清理资源
await session.close()
3.9 总结
本章深入讲解了异步编程的核心概念和实战应用:
- 事件循环:理解了select/poll/epoll机制和任务调度
- 协程机制:理解了生成器、yield from和async/await的工作原理
- asyncio模块:掌握了create_task、gather、wait和并发原语的使用
- 异步HTTP客户端:学会了使用aiohttp和httpx
- 实战应用:构建了支持1000并发的高性能异步爬虫
关键要点:
- ⚠️ 忘记await会导致协程未执行
- ⚠️ 在协程中使用阻塞操作会阻塞整个事件循环
- ⚠️ 异步上下文中使用同步库会失去异步优势
- ✅ 异步编程适合I/O密集型任务,可以大幅提升性能
性能提升:
- 同步爬虫:100秒(100个请求)
- 异步爬虫:1.5秒(100个请求)
- 提升约66倍!
在下一章中,我们将学习HTTP客户端库的深度定制,进一步提升爬虫的隐蔽性和性能。