从「同步」到「异步」:用 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.pswp.cn/bicheng/92134.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/92134.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 在麒麟系统上部署使用 + DBeaver 远程连接 + SQL 数据导入完整流程

🚀 MySQL 在麒麟系统上部署使用 DBeaver 远程连接 SQL 数据导入完整流程适用于国产操作系统(如:麒麟 / 统信 / Ubuntu)和 MySQL 8.0。包含远程配置、授权连接、SQL 导入、DBeaver连接配置等常见问题解决方案。📦 环境…

C语言-指针初级(指针定义、指针的作用、指针的计算、野指针、悬空指针、void类型指针)

本章概述思维导图:C语言指针指针是C语言中最强大但也最容易混淆的特性之一。它提供了直接操作内存地址的能力,使得C语言具有高效性和灵活性。下面我将详细介绍C语言指针的各个方面。指针定义指针的本质:指针是一个变量,其值为另一…

具身智能VLA困于“数据泥潭”,人类活动视频数据是否是“破局之钥”?

前言尽管当前的视觉-语言-动作(VLA)模型已展现出显著进展,但其在新场景和与复杂物体交互中的性能会显著下降,在遵循指令方面落后于像LLaVA 这样的大型多模态模型(LMM)。这种局限性源于现有VLA模型对存在固有…

CIO如何规划企业BI分析指标体系 —— 从经营出发到绩效管理

如果你是一家企业的CIO,要启动一个商业智能BI项目,负责规划整个项目的商业智能BI分析内容,你该如何入手准备?要有什么样的思路。如果是管理层、老板还不能清晰认识到商业智能BI的价值,也提不出很清晰的需求&#xff0c…

go学习笔记:panic是什么含义

anic 是 Go 语言中的一种运行时错误处理机制,用于处理程序中的异常情况。 基本含义 panic 会: 立即停止当前函数的执行 开始执行 defer 函数(如果有的话) 向上传播到调用栈,逐层执行 defer 如果到达 main 函数&am…

OpenLayers 入门指南【五】:Map 容器

文章目录 一、Map 对象核心参数 1. target 2. view 3. layers 4. controls 5. interactions 6. 其他重要参数 二、Map 对象常用方法 1. 图层管理 2. 控件管理 3. 交互管理 4. 视图与坐标操作 5. 事件监听 6. 覆盖物管理 7. 其他 三、总结 上一章节中我们通过修改OlMap.vue组件已…

关税战火中的技术方舟:新西兰证券交易所的破局之道 ——从15%关税冲击到跨塔斯曼结算联盟,解码下一代交易基础设施

一、今日焦点:全球关税震荡与新西兰的“技术自卫” 1. 特朗普关税大限落地,新西兰启动紧急游说 2025年8月1日,美国总统特朗普正式签署行政令,对贸易顺差国征收最低15%基准关税。新西兰贸易部长紧急声明:“将提出有力证…

windows内核研究(软件调试-软件断点)

软件调试软件断点调试的本质是什么?就是在被调试程序中触发异常,然后被调试程序就会向_DEBUG_OBJECT结构体添加调试事件,这里我们调试器就接管这个异常了(调试的过程就是异常处理的过程) 软件断点 在x64dbg中通过快捷键…

HarmonyOS】鸿蒙应用开发中常用的三方库介绍和使用示例

🌟 鸿蒙应用开发常用三方库指南(2025 最新版)适用版本:HarmonyOS NEXT / API 12 参考来源:HarmonyOS 三方库中心 截止至 2025 年 8 月 1 日,本文整理了当前社区中下载量高、稳定性强、生态完善的热门三方库…

【通识】C Sharp

1. 使用 \p{名称}构造匹配Unicode常规类别(该示例为Pd或“标点、短划线”类别)和命名块(IsGreek和IsBsicLatin命名块) using System; using system.Text.RegularExpressions; public class Example {public static void main() {s…

国内首个开源SCA社区——OpenSCA开源社区

OpenSCA开源社区成果说明项目背景智能时代,软件定义一切。随着开发模式的敏捷化转型,开源代码在软件制品中的占比越来越大,开源软件已然成为软件供应链的重要组成部分。由于其特殊性,开源代码的引入增加了软件应用的风险面&#x…

超聚变:智能体时代,AI原生重构城企数智化基因

2025 世界人工智能大会(WAIC)世博展览馆内,超聚变展台前人头攒动,其展示的AI落地全栈解决方案及上百个AI应用场景吸引了众多参观者驻足观看。这是今年WAIC大会火爆的一角,更是当下AI应用爆发的一个缩影。当人工智能发展…

Traccar:开源GPS追踪系统的核心价值与技术全景

Traccar:开源GPS追踪系统的核心价值与技术全景 —— 从设备兼容到企业级定位管理的开源实践 一、项目定位:多场景定位管理的开源基石 Traccar是一个高扩展性的开源GPS追踪平台,支持全球超过200种通信协议与2000款GPS设备(包括车…

编程与数学 03-002 计算机网络 20_计算机网络课程实验与实践

编程与数学 03-002 计算机网络 20_计算机网络课程实验与实践一、实验环境搭建(一)使用模拟器(如Cisco Packet Tracer)搭建网络实验环境(二)实验设备的配置与连接二、基础网络实验(一&#xff09…

15个命令上手Linux!

1、id,显示当前登录系统的用户信息2、pwd,显示当前工作目录的绝对路径3、ls,显示当前目录下的内容(ls -r:按反向顺序列出内容,ls -l:以详细列表形式显示)4、cd,切换工作目…

MongoDB分片技术实现

MongoDB分片技术实现概述MongoDB分片(Sharding)是MongoDB的水平扩展解决方案,通过将数据分布到多个分片(shard)上来处理大数据量和高吞吐量的需求。MongoDB分片架构1. 分片集群组件# MongoDB分片集群架构 version: 3.8…

Python开发环境PyCharm下载与安装

python下载 python下载地址: Download Python | Python.org 上面的下载速度慢的话,用下面的地址下载(window): https://download.csdn.net/download/liangmengbk/91580033 PyCharm下载 PyCharm下载地址&#xff1a…

汽车供应链PPAP自动化审核指南:如何用AI实现规则精准匹配与文件智能校验

在汽车行业质量管理的核心环节,PPAP(生产件批准程序)审核长期困扰着供应商与主机厂。 随着IATF 16949等标准持续升级、新能源零件复杂度激增,传统人工审核模式正面临系统性挑战。 行业数据显示,超过70%的SQE&#xf…

正则表达式在js中的应用

正则表达式在 JavaScript 中的应用非常广泛,尤其是在字符串处理和验证方面。以下是一些常见的正则表达式方法及其应用示例,包括 .test() 方法。 1. .test() 方法 .test() 方法用于测试一个字符串是否匹配正则表达式。如果匹配,返回 true&…

Rust视频处理开源项目精选

Rust视频处理开源项目精选 基于Rust实现的视频处理示例 以下是一些基于Rust实现的视频处理或多媒体相关的开源项目或示例,涵盖编解码、流媒体、分析工具等方向,可作为实际开发参考: 视频编解码与处理 rav1e:Rust编写的AV1视频编码器,高性能且内存安全,适合研究视频压缩…