醋醋百科网

Good Luck To You!

Python asyncio实战:3个技巧让I/O密集型任务效率提升10倍

你有没有遇到过这样的情况:写了一个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%的人都会犯

  1. 用同步库阻塞事件循环:比如在协程里调用time.sleep()(同步)而非asyncio.sleep()(异步),会导致整个事件循环卡住。
  2. 并发数设太高:虽然协程轻量,但无限制创建任务会导致系统资源耗尽,建议用asyncio.Semaphore限制并发(比如设500)。
  3. 连接池没配好:数据库/HTTP连接池的max_size要根据服务器性能调整,PostgreSQL建议设为CPU核心数*2(来源:asyncpg最佳实践)。

最后:从同步到异步的迁移步骤

  1. 找出I/O瓶颈:用cProfile分析代码,定位耗时的网络请求、文件读写。
  2. 替换同步库:requests→aiohttp,psycopg2→asyncpg,open()→aiofiles。
  3. 用协程重构逻辑:把def改成async def,阻塞调用前加await。
  4. 测试性能:用vegeta(HTTP压测)或asyncio.run_coroutine_threadsafe(并发测试)验证效果。

现在就去试试吧!以我的经验,爬虫、API服务这类场景,用asyncio重构后,服务器成本能降一半,用户等待时间从秒级压到毫秒级——这就是异步编程的魔力。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言