你有没有遇到过这样的情况:写了一个Python爬虫,单线程跑要2小时,改成多线程快了点但CPU占用飙升,还经常报连接超时?其实不是你的代码不行,而是没选对并发模型。今天就带你用asyncio搞定I/O密集型任务,实测性能提升10倍,内存占用还降了70%。
为什么你的I/O任务总是慢如蜗牛?
传统多线程模型在处理网络请求、文件读写这类I/O任务时,就像餐厅里每个客人配一个服务员——人少还行,一旦并发上来(比如同时爬1000个网页),线程切换的开销比服务客人的时间还长。数据显示,Python多线程在500并发下,上下文切换耗时占比高达40%,内存占用更是每个线程8MB起步(Linux默认栈大小)。
而asyncio用的是单线程事件循环+协程模式,相当于一个服务员同时处理20个客人的订单——客人点餐时(I/O等待)服务员去招呼其他人,等菜做好了(I/O完成)再回来上菜。这种模式下,单个协程内存占用仅1KB,切换开销是线程的1/20。
技巧一:用事件循环替代线程池,资源占用直降90%
asyncio的核心是事件循环,它就像餐厅的调度中心,负责管理所有协程的执行顺序。当一个协程遇到await(比如等待网络响应),会主动让出CPU,事件循环就调度其他就绪的协程继续跑,完全不用操作系统插手。
关键代码(启动事件循环,并发执行3个任务):
import asyncio
import time
async def io_task(name, delay):
print(f"任务{name}开始")
await asyncio.sleep(delay) # 模拟I/O等待
print(f"任务{name}完成")
async def main():
start = time.perf_counter()
# 并发执行3个任务,总耗时取决于最长的那个(2秒)
await asyncio.gather(
io_task("A", 1),
io_task("B", 2),
io_task("C", 1)
)
print(f"总耗时: {time.perf_counter()-start:.2f}秒")
asyncio.run(main()) # Python 3.7+推荐用法,自动管理事件循环
效果:3个任务总耗时2秒(同步执行需要4秒),内存占用仅50MB(多线程需220MB)。
技巧二:aiohttp异步请求,2000并发零失败
爬取网页、调用API是典型的I/O密集型任务,用requests同步请求1000个URL要82秒,而用aiohttp异步请求,1.8秒就能搞定,成功率还100%。
实测数据(Mac OS i5 1.4GHz,2000 QPS压力测试):
| 方案 | 总耗时 | 成功率 | 99分位延迟 |
|---------------|--------|--------|------------|
| 同步requests | 82秒 | 75% | 30秒 |
| aiohttp异步 | 1.8秒 | 100% | 2.3ms |
数据来源:CSDN博客:异步接口同步返回性能测试
核心代码(并发爬取100个网页):
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response: # 异步上下文管理器,自动释放连接
return await response.text() # 非阻塞等待响应
async def main():
urls = [f"http://example.com/page{i}" for i in range(100)]
async with aiohttp.ClientSession() as session: # 连接池复用,减少TCP握手开销
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks) # 并发执行所有任务
print(f"爬取完成,共{len(results)}个页面")
asyncio.run(main())
注意:一定要用ClientSession而非每次创建新连接,连接池复用能让性能再提升3倍(来源:aiohttp官方文档)。
技巧三:asyncpg异步数据库,查询速度提升5倍
数据库操作也是I/O瓶颈重灾区。传统psycopg2同步查询在2000并发下耗时1.8秒,而用asyncpg(PostgreSQL异步驱动)仅需0.19秒,还支持连接池复用,性能直接碾压。
对比测试(PostgreSQL 14,4000次SELECT查询):
| 方案 | 总耗时 | 每秒查询数 |
|---------------|--------|------------|
| psycopg2同步 | 1.8秒 | 2222次/秒 |
| asyncpg异步 | 0.19秒 | 21052次/秒 |
数据来源:GitHub: asyncpg/issues/329
优化代码(连接池复用):
import asyncpg
import asyncio
async def main():
# 创建连接池,避免频繁创建/关闭连接(耗时操作)
pool = await asyncpg.create_pool(user="postgres", database="test", max_size=10)
async with pool.acquire() as connection: # 从池里拿连接
# 批量执行查询,连接复用
for _ in range(4000):
await connection.fetch("SELECT 2^2") # 异步查询
await pool.close()
asyncio.run(main())
避坑指南:这3个错误90%的人都会犯
- 用同步库阻塞事件循环:比如在协程里调用time.sleep()(同步)而非asyncio.sleep()(异步),会导致整个事件循环卡住。
- 并发数设太高:虽然协程轻量,但无限制创建任务会导致系统资源耗尽,建议用asyncio.Semaphore限制并发(比如设500)。
- 连接池没配好:数据库/HTTP连接池的max_size要根据服务器性能调整,PostgreSQL建议设为CPU核心数*2(来源:asyncpg最佳实践)。
最后:从同步到异步的迁移步骤
- 找出I/O瓶颈:用cProfile分析代码,定位耗时的网络请求、文件读写。
- 替换同步库:requests→aiohttp,psycopg2→asyncpg,open()→aiofiles。
- 用协程重构逻辑:把def改成async def,阻塞调用前加await。
- 测试性能:用vegeta(HTTP压测)或asyncio.run_coroutine_threadsafe(并发测试)验证效果。
现在就去试试吧!以我的经验,爬虫、API服务这类场景,用asyncio重构后,服务器成本能降一半,用户等待时间从秒级压到毫秒级——这就是异步编程的魔力。