使用Asyncio下载大文件比线程慢10倍:求优化方案与原因分析
大文件异步下载性能问题分析与优化方案
问题背景
使用ThreadPoolExecutor实现的多线程图片下载(线程池最大3个工作线程)处理5MB以上的大体积图片时运行正常,但相同场景下基于asyncio+aiohttp+aiofiles的异步实现速度极慢,且代码无语法或逻辑错误,需要分析性能瓶颈原因并给出优化方案。
性能瓶颈原因
- 大文件一次性读取阻塞事件循环
原异步代码中使用await response.read()一次性将整个大文件加载到内存,虽然该操作为异步,但读取过程中事件循环会被长时间占用,无法切换到其他下载任务。而线程池方案中,单个线程的IO阻塞不会影响其他线程执行,多线程可并行处理多个下载任务,因此大文件场景下线程池调度效率更高。 - 内存占用过高间接拖慢性能
一次性读取大文件会导致内存占用激增,不仅增加GC压力,还可能引发系统层面的内存交换,进一步降低整体运行速度。 - 默认配置未适配大文件场景
aiohttp默认的连接池、超时等参数为通用配置,未针对大文件下载做优化,并发处理效率受限。
优化方案
核心优化思路是分块处理网络读取与文件写入,让事件循环能及时切换任务,同时调整异步客户端配置提升并发效率。
修改后的异步下载代码
import asyncio import os import time import aiohttp import aiofiles async def download_image_async(url, folder, session): os.makedirs(folder, exist_ok=True) filename = url.split('/')[-1] filepath = os.path.join(folder, filename) # 分块读取网络内容,每块1MB chunk_size = 1024 * 1024 async with session.get(url) as response: async with aiofiles.open(filepath, "wb") as f: async for chunk in response.content.iter_chunked(chunk_size): await f.write(chunk) async def main(): # 调整aiohttp连接池参数,增大并发数(需根据目标服务器限制调整,避免触发反爬) connector = aiohttp.TCPConnector(limit=10) async with aiohttp.ClientSession(connector=connector) as session: urls = [ "https://images-assets.nasa.gov/image/PIA03149/PIA03149~orig.jpg", "https://upload.wikimedia.org/wikipedia/commons/3/37/African_Bush_Elephant.jpg", "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg", "https://upload.wikimedia.org/wikipedia/commons/2/29/%22Arte_Colonial%22.jpg", "https://upload.wikimedia.org/wikipedia/commons/d/d2/%22CUT%22_June_1929_04.jpg", "https://upload.wikimedia.org/wikipedia/commons/8/82/%22CUT%22_June_1929_05.jpg", "https://upload.wikimedia.org/wikipedia/commons/b/b1/%22Council_of_the_Gods%22_in_Galleria_Borghese_%28Rome%29_ceiling.jpg", "https://upload.wikimedia.org/wikipedia/commons/7/71/%22East_front_of_Court_House_and_Planter%27s_House.%22.jpg", "https://upload.wikimedia.org/wikipedia/commons/b/b6/%22Greater_Germany%22._Major_administrative_divisions._July_1._1944._100_kms_-_btv1b531213280.jpg", ] * 2 tasks = [download_image_async(url, "3_asyncio_opt", session) for url in urls] await asyncio.gather(*tasks) if __name__ == "__main__": start_time = time.time() asyncio.run(main()) end_time = time.time() print(f"优化后Asyncio download time: {end_time - start_time:.2f} seconds")
优化点说明
- 分块读取与写入:使用
response.content.iter_chunked(chunk_size)分块读取网络数据,每块1MB,读取一块就写入一块,避免一次性加载大文件到内存,让事件循环有机会切换到其他任务,提升并发效率。 - 调整连接池参数:通过
TCPConnector(limit=10)增大并发连接数(需根据目标服务器的并发限制调整,避免被封禁),提升多任务并行处理能力。 - 提前创建目录:将目录创建逻辑移到文件操作前,避免每个分块写入时重复判断目录存在性。
内容的提问来源于stack exchange,提问作者Daniil Yefimov
相关产品推荐
相关产品推荐

