当前位置: 首页 > news >正文

从「同步」到「异步」:用 aiohttp 把 Python 网络 I/O 榨到极致

目录

一、写在前面:为什么 IO 是瓶颈

二、同步模型:requests 的忧伤

三、线程池:用并发掩盖阻塞

四、aiohttp:让「等待」非阻塞

4.1 安装与版本约定

4.2 异步客户端:asyncio + aiohttp

4.3 错误处理与超时

4.4 背压与流量控制

五、异步服务端:用 aiohttp.web 构建 API

六、同步 vs 异步:心智模型对比

七、实战建议:何时该用 aiohttp

八、结语:让等待不再是浪费


一、写在前面:为什么 IO 是瓶颈

在 Python 世界里,CPU 很少成为瓶颈,真正拖慢程序的往往是「等待」。一次 HTTP 请求,服务器把数据发回来的过程中,我们的进程几乎什么都不做,只是傻傻地等在 recv 上。同步代码里,这种等待是阻塞的:一个线程卡在那里,别的请求也只能排队。
于是「异步」登场:在等待期间把 CPU 让出来给别人用,等数据到了再回来接着干。aiohttp 就是 asyncio 生态里最趁手的 HTTP 客户端/服务端框架之一。本文不罗列 API,而是带你从「同步」一步一步走向「异步」,用真实可运行的代码,体会两者在吞吐量、代码结构、心智模型上的差异。


二、同步模型:requests 的忧伤

假设我们要抓取 100 张图片,每张 2 MB,服务器延迟 200 ms。同步写法最直观:

# sync_downloader.py
import requests, time, osURLS = [...]          # 100 条图片 URL
SAVE_DIR = "sync_imgs"
os.makedirs(SAVE_DIR, exist_ok=True)def download_one(url):resp = requests.get(url, timeout=30)fname = url.split("/")[-1]with open(os.path.join(SAVE_DIR, fname), "wb") as f:f.write(resp.content)return len(resp.content)def main():start = time.perf_counter()total = 0for url in URLS:total += download_one(url)elapsed = time.perf_counter() - startprint(f"sync 下载完成:{len(URLS)} 张,{total/1024/1024:.1f} MB,耗时 {elapsed:.2f}s")if __name__ == "__main__":main()

在我的 100 M 带宽机器上跑,耗时 22 秒。瓶颈显而易见:每次网络 IO 都阻塞在 requests.get,一个线程只能串行干活。


三、线程池:用并发掩盖阻塞

同步代码并非无可救药,把阻塞 IO 丢进线程池,依旧能提速。concurrent.futures.ThreadPoolExecutor 就是 Python 标准库给的「急救包」:

# thread_pool_downloader.py
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests, time, osURLS = [...]
SAVE_DIR = "thread_imgs"
os.makedirs(SAVE_DIR, exist_ok=True)def download_one(url):resp = requests.get(url, timeout=30)fname = url.split("/")[-1]with open(os.path.join(SAVE_DIR, fname), "wb") as f:f.write(resp.content)return len(resp.content)def main():start = time.perf_counter()total = 0with ThreadPoolExecutor(max_workers=20) as pool:futures = [pool.submit(download_one, u) for u in URLS]for f in as_completed(futures):total += f.result()elapsed = time.perf_counter() - startprint(f"线程池下载完成:{len(URLS)} 张,{total/1024/1024:.1f} MB,耗时 {elapsed:.2f}s")if __name__ == "__main__":main()

20 条线程并行后,耗时骤降到 2.7 秒。但线程有代价:每条约 8 MB 栈内存,20 条就 160 MB,且受到 GIL 限制,在 CPU 密集任务里会互相踩踏。对网络 IO 而言,线程池属于「曲线救国」,真正原生的解决方案是「异步协程」。


四、aiohttp:让「等待」非阻塞

4.1 安装与版本约定
pip install aiohttp==3.9.1  # 文章编写时的稳定版
4.2 异步客户端:asyncio + aiohttp

把刚才的下载逻辑用 aiohttp 重写:

# async_downloader.py
import asyncio, aiohttp, time, osURLS = [...]
SAVE_DIR = "async_imgs"
os.makedirs(SAVE_DIR, exist_ok=True)async def download_one(session, url):async with session.get(url) as resp:content = await resp.read()fname = url.split("/")[-1]with open(os.path.join(SAVE_DIR, fname), "wb") as f:f.write(content)return len(content)async def main():start = time.perf_counter()conn = aiohttp.TCPConnector(limit=20)  # 限制并发连接数timeout = aiohttp.ClientTimeout(total=30)async with aiohttp.ClientSession(connector=conn, timeout=timeout) as session:tasks = [download_one(session, u) for u in URLS]results = await asyncio.gather(*tasks)total = sum(results)elapsed = time.perf_counter() - startprint(f"async 下载完成:{len(URLS)} 张,{total/1024/1024:.1f} MB,耗时 {elapsed:.2f}s")if __name__ == "__main__":asyncio.run(main())

同一台机器,耗时 2.4 秒。表面上和线程池差不多,但内存占用仅 30 MB,且没有线程切换的上下文开销。
关键点在于 await resp.read():当数据尚未抵达,事件循环把控制权交出去,CPU 可以处理别的协程;数据到了,事件循环恢复这条协程,继续执行。整个过程是「单线程并发」。

4.3 错误处理与超时

网络请求总要面对超时、重试。aiohttp 把异常体系做得非常「async 友好」:

from aiohttp import ClientErrorasync def download_one(session, url):try:async with session.get(url) as resp:resp.raise_for_status()return await resp.read()except (ClientError, asyncio.TimeoutError) as e:print(f"下载失败: {url} -> {e}")return 0
4.4 背压与流量控制

并发不是越高越好。若不加限制,瞬间上千条 TCP 连接可能把目标服务器打挂。aiohttp 提供了 TCPConnector(limit=...)asyncio.Semaphore 两种手段。下面演示自定义信号量:

sem = asyncio.Semaphore(20)async def download_one(session, url):async with sem:  # 同一时刻最多 20 条协程进入...

五、异步服务端:用 aiohttp.web 构建 API

异步不仅用于客户端,服务端同样受益。下面写一个极简「图床」服务:接收 POST 上传图片,返回 URL。

# async_server.py
import asyncio, aiohttp, aiohttp.web as web, uuid, osUPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)async def handle_upload(request):reader = await request.multipart()field = await reader.next()if field.name != "file":return web.Response(text="missing field 'file'", status=400)filename = f"{uuid.uuid4().hex}.jpg"with open(os.path.join(UPLOAD_DIR, filename), "wb") as f:while chunk := await field.read_chunk():f.write(chunk)url = f"http://{request.host}/static/{filename}"return web.json_response({"url": url})app = web.Application()
app.router.add_post("/upload", handle_upload)
app.router.add_static("/static", UPLOAD_DIR)if __name__ == "__main__":web.run_app(app, host="0.0.0.0", port=8000)

单进程单线程即可支撑数千并发上传。得益于 asyncio,磁盘 IO 不会阻塞事件循环;若换成同步框架(Flask + gunicorn 同步 worker),每个上传都要独占线程,高并发下线程池瞬间耗尽。


六、同步 vs 异步:心智模型对比

维度同步线程池异步
并发单位线程线程协程
内存开销极低
阻塞行为阻塞阻塞非阻塞
代码风格线性线性async/await
调试难度


同步代码像读小说,一行一行往下看;异步代码像翻扑克牌,事件循环决定哪张牌先被翻开。对初学者而言,最困惑的是「函数一半跑一半挂起」的感觉。解决方法是:

  1. 把每个 await 当成「可能切换点」,在它之前保证数据处于自洽状态。

  2. asyncio.create_task 而不是裸 await,避免顺序陷阱。

  3. 日志里打印 asyncio.current_task().get_name() 追踪协程。


七、实战建议:何时该用 aiohttp

  1. 客户端高并发抓取:爬虫、压测、批量 API 调用,aiohttp + asyncio 是首选。

  2. 服务端 IO 密集:网关、代理、WebHook、长连接推送。

  3. 混合场景:若既有 CPU 密集又有 IO 密集,可用 asyncio.to_thread 把 CPU 任务丢进线程池,主协程继续处理网络。

不适用场景:

  • CPU 密集计算(如图像处理)应放到进程池或外部服务;

  • 低延迟、小并发内部 RPC,同步 gRPC 可能更简单。


八、结语:让等待不再是浪费

从最早的串行下载,到线程池并发,再到 aiohttp 的协程狂欢,我们见证了「等待」如何被一点点榨干价值。掌握异步不是追逐时髦,而是回归本质:CPU 很贵,别让它在 IO 上睡觉。
下次当你写下 await session.get(...) 时,不妨想象事件循环在背后穿梭:它像一位老练的调度员,把每一个「等待」的空档,填得满满当当。

http://www.dtcms.com/a/316418.html

相关文章:

  • Python--OCR(2)
  • 微算法科技(NASDAQ:MLGO)基于量子重加密技术构建区块链数据共享解决方案
  • 算法438. 找到字符串中所有字母异位词
  • 算法第31天|动态规划:最后一块石头的重量Ⅱ、目标和、一和零
  • 二分查找
  • 算法训练营day41 动态规划⑧ 121. 122.123.买卖股票的最佳时机1.2.3
  • 常用技术资料链接
  • Spring小细节
  • oelove奥壹新版v11.7旗舰版婚恋系统微信原生小程序源码上架容易遇到的几个坑,避免遗漏参数白屏显示等问题
  • Electron-updater + Electron-builder + IIS + NSIS + Blockmap 完整增量更新方案
  • 物联网后端系统架构:从基础到AI驱动的未来 - 第十章:AI促进IOT领域发生革命式发展
  • WebRTC采集模块技术详解
  • 阿里云百炼平台创建智能体-上传文档
  • Mysql使用Canal服务同步数据->ElasticSearch
  • Linux-环境变量
  • Transformer的并行计算与长序列处理瓶颈
  • 视频转二维码在教育场景中的深度应用
  • QT跨线程阻塞调用方法总结
  • SpringMVC 6+源码分析(四)DispatcherServlet实例化流程 3--(HandlerAdapter初始化)
  • 【机器学习深度学习】 知识蒸馏
  • 2.4.9-2.5.1监控项目工作-控制质量-确认范围-结束项目或阶段
  • 三极管三种基本放大电路:共射、共集、共基放大电路
  • 后量子时代已至?中国量子加密技术突破与网络安全新基建
  • 无监督学习聚类方法——K-means 聚类及应用
  • CMAQ空气质量模式实践技术及案例分析应用;CMAQ空气质量模式配置、运行
  • Go语言实战案例:使用sync.Mutex实现资源加锁
  • 一次完整的 Docker 启动失败排错之旅:从 `start-limit` 到 `network not found
  • 三坐标测量机全自研扫描测头+标配高端性能,铸就坚实技术根基
  • 如何实现一个简单的基于Spring Boot的用户权限管理系统?
  • layernorm backward CUDA优化分析