当前位置: 首页 > 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.lryc.cn/news/610983.html

相关文章:

  • Vue.js之核心语法与指令
  • 网络 —— 笔记本(主机)、主机虚拟机(Windows、Ubuntu)、手机(笔记本热点),三者进行相互ping通
  • 初始MyBatis
  • LabVIEW 2025 安装攻略(附图文教程)适用于测试与自动控制领域
  • MySQL 查询性能优化与索引失效问题全解析
  • 使用公众号的消息模板给关注用户发消息
  • MySQL CONV()函数
  • spring webflux链路跟踪【traceId日志自动打印】
  • 移动端 WebView 调试实战 深色模式样式失效与主题切换异常排查指南
  • 前端1.0
  • Lua语言程序设计1:基础知识、数值、字符串与表
  • 针对软件定义车载网络的动态服务导向机制
  • linux_https,udp,tcp协议(更新中)
  • 实战项目3-工控软件-2.0- 自定义控件HMILabel的创建
  • 漏洞分析:90分钟安全革命
  • 赛灵思ZYNQ官方文档UG585自学翻译笔记:Quad-SPl Flash 闪存控制器
  • 信息系统项目管理中的沟通管理实战精解
  • 智慧油站误报率↓77%:陌讯多模态融合算法实战解析
  • 【Git】git提交代码报错Git: husky > pre-commit
  • 【Java面试题】注解,异常相关知识
  • 二维数点问题 1
  • Dell电脑Windows系统更新后声卡驱动无法识别插线耳机问题
  • 第13届蓝桥杯Scratch_选拔赛_初级组_真题2022年1月22日
  • leetcode-python-删除链表的倒数第 N 个结点
  • Leetcode 13 java
  • Linux网络编程:TCP初体验
  • 从递归到动态规划-解码方法Ⅱ
  • 【IDEA】IntelliJ IDEA 中文官方文档全面介绍与总结
  • 以Linux为例补充内存管理基础知识
  • 2025年服务器僵尸攻防战:从AI勒索到量子免疫,构建下一代“数字抗体”