共享 AsyncClient 实例可复用连接池、DNS缓存和TLS状态,避免重复初始化开销,实测延迟降60%+;并发写文件须用 aiofiles 异步I/O 或内存缓冲,禁用阻塞式 open()。

直接用 httpx.AsyncClient 配合 asyncio.gather 就能实现高效并发下载,但关键不在“能不能发”,而在“怎么控”——连接复用、超时设置、错误隔离和流式写入这四点没处理好,反而比同步还慢、还容易崩。
为什么不能每个URL都新建一个 AsyncClient
每次 async with httpx.AsyncClient() 都会重建连接池、DNS缓存、TLS握手状态,开销远大于复用。实测 10 个请求若各自建 client,总耗时可能比串行还高;而共享一个 client 实例,连接自动复用,延迟下降 60%+。
- 必须把
AsyncClient实例作为参数传入每个下载协程,而不是在协程里创建 - client 生命周期要覆盖全部任务:用
async with包裹整个gather调用块 - 避免在循环内或回调中重复初始化 client —— 这是新手最常踩的坑
如何安全地并发下载并写入文件
并发写文件不是简单 await file.write() 就完事。异步文件 I/O 在 Python 标准库中不原生支持(aiofiles 是第三方方案),直接用普通 open() 会阻塞事件循环,导致并发退化为伪并发。
- 推荐方案:先用
response.aiter_bytes()流式读取响应体,再用aiofiles异步写入(需pip install aiofiles) - 替代方案:内存缓冲 + 同步写入 —— 仅适用于小文件(如 asyncio.to_thread(open, ..., "wb") 避免阻塞
- 绝对不要在协程里直接调用
open(..., "wb").write(...),这会让整个事件循环卡住
超时与重试必须显式配置,否则默认行为很危险
httpx.AsyncClient 默认 timeout 是 5 秒,且不重试。网络抖动时大量请求瞬间失败,asyncio.gather 一报错就全崩;更糟的是,某些服务端空闲连接超时(如 Nginx 的 keepalive_timeout=75s),客户端若没设 keepalive_expiry,连接池会复用已断连,后续请求直接抛 ConnectError。
立即学习“Python免费学习笔记(深入)”;
- 务必设
timeout=httpx.Timeout(10.0, connect=5.0, read=8.0),分项控制更精准 - 连接池要配
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20, keepalive_expiry=60.0) - 重试逻辑别依赖 client 内置(HTTPX 不提供自动重试),自己用
tenacity或手动for循环 +await asyncio.sleep()
并发数量不是越多越好,得看目标服务承受力
盲目设 gather(*tasks) 跑 100 个并发,大概率触发目标站限流(429)、连接拒绝(ConnectionRefusedError)或 DNS 查询超时。真实场景下,合理并发数取决于目标服务器响应时间、你本地出口带宽、以及对方反爬强度。
- 建议从 10–20 并发起步,观察平均响应时间和错误率
- 用
asyncio.Semaphore控制最大并发数,比如sem = asyncio.Semaphore(15),每个任务async with sem:再执行 - 对同一域名的请求,尤其要限制并发(比如单域名 ≤ 6),避免被当成扫描器封IP
真正难的不是并发本身,而是让并发稳定、可控、可观测:连接是否真复用了?失败请求有没有被静默吞掉?磁盘 IO 是否成了瓶颈?这些细节不盯住,代码跑得越快,出问题时越难定位。


















