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

11-GIL不是性能杀手(上)-CPU密集vsIO密集的实测对比

文章目录

  • GIL 不是性能杀手(上):CPU密集 vs IO密集——你以为的瓶颈其实是错觉
    • 导入语
    • 1 ~> GIL 到底是什么——一句话能解释清楚
      • 1.1 为什么 CPython 要加这把锁
    • 2 ~> GIL 什么时候释放——临界点在三类操作之间
      • 2.1 GIL 的持有与释放
      • 2.2 什么时候 GIL 会被释放
    • 3 ~> CPU 密集型实测——GIL 确实把多线程打回原形
      • 3.1 测试代码:暴力计算斐波那契额
      • 3.2 实测数据(i7-12700,8核16线程)
      • 3.3 图解:CPU 密集型场景中 GIL 的行为
    • 4 ~> IO 密集型实测——线程数量和速度基本成正比
      • 4.1 测试代码:并发网络请求
      • 4.2 实测数据
      • 4.3 图解:IO 密集型场景中 GIL 的行为
    • 5 ~> 什么时候该用多线程、什么时候该切多进程
      • 5.1 决策表
      • 5.2 一个真实的踩坑经历
    • 思考 && 总结
    • 结尾

GIL 不是性能杀手(上):CPU密集 vs IO密集——你以为的瓶颈其实是错觉

📖文章简介:GIL(全局解释器锁)是Python面试的必考题,也是论坛上被骂得最多的Python"缺陷"。但大多数人骂GIL的原因都不对——他们以为多线程慢是因为GIL,实际上大多数场景里慢的是别的东西。上篇聚焦一个核心问题:GIL在什么场景下真的是瓶颈?通过CPU密集(纯计算)和IO密集(网络/文件读写)两组实测数据对比,讲清楚GIL影响多线程性能的边界条件。文章从CPython源码角度解释GIL的获取/释放时机,配有可复现的压力测试代码和性能曲线对比,读完你会知道什么时候该用多线程、什么时候该果断切多进程。


🎬 个人主页:源码骑士

专栏传送门:《Android开发基础》《python基础课程》

⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂


🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"


导入语

“Python 多线程是假的,因为 GIL,所以多线程没有用。”

这话我至少在十个评论区里看到过。问题是——说这话的人,十个里有九个没做过实测。他们只是背了一句话,然后就对所有 Python 多线程持否定态度。

2022年我在一个日志处理服务上做性能优化。当时的架构是单进程串行——一个线程读文件,读完再做分析。我提出用多线程并行读多个日志文件,一位同事当场说"Python 多线程没用,GIL 会让它变成串行"。我没多解释,写了一组基准测试跑给他看——四线程并行读文件比单线程快了 3.2 倍。

GIL 的真实影响不是"多线程没用了",而是"多线程在 CPU 密集型任务上没用"。上篇把这个边界讲清楚,配实测数据。


1 ~> GIL 到底是什么——一句话能解释清楚

GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器中的一把互斥锁。它的核心规则只有八个字:同一时刻,只有一个线程能执行 Python 字节码。

注意,这里的关键词不是"线程不能并行",而是**“Python 字节码不能并行”。**

如果你有一个 Python 进程: 线程A → 获得 GIL → 执行 Python 代码 → 释放 GIL 线程B → 等待 GIL → 获得 GIL → 执行 Python 代码 → 释放 GIL 线程C → 等待 GIL → 获得 GIL → 执行 Python 代码 → 释放 GIL 不管你有4核还是64核,同时刻只有一个线程在跑 Python 代码

1.1 为什么 CPython 要加这把锁

CPython 的内存管理基于引用计数。如果没有 GIL,两个线程同时修改一个对象的引用计数就会产生竞态条件——引用计数可能被算错,导致对象被提前释放或永不释放。加一把全局锁是最简单粗暴的线程安全方案——代价就是多核并行能力受限。

其他 Python 实现(Jython、IronPython)没有 GIL——因为它们分别用了 JVM 和 .NET 的垃圾回收器,不依赖引用计数。


2 ~> GIL 什么时候释放——临界点在三类操作之间

2.1 GIL 的持有与释放

CPython 3.2 之前的 GIL 行为很简单:执行 100 条字节码指令后释放。但 3.2 之后换成了固定时间片机制——每个线程持有 GIL 约 5 毫秒后被迫切换。

线程持有 GIL 约 5ms → 释放给下一个线程 → 这个线程也持有约 5ms → 循环往复

更关键的是:如果你执行的是 IO 操作(网络读写、文件读写、sleep),GIL 会在 IO 阻塞发生前主动释放。这就是多线程在 IO 密集场景下依然高效的原因——线程在等网路响应的时候不锁着解释器。

2.2 什么时候 GIL 会被释放

操作类型GIL 是否释放说明
纯 CPU 计算(for i in range(10^9)❌ 不释放(只释放时间片)只有 5ms 切换,没有主动放弃
time.sleep(1)✅ 释放阻塞出主动释放
socket.recv()✅ 释放IO 操作,底层 C 库释放 GIL
file.read()✅ 释放同 IO 操作
numpy.dot(A, B)✅ 释放大量 NumPy 运算在 C 层面完成,C 代码释放 GIL

一把锁如果能在任务阻塞时释放,那它在阻塞密集的场景中就不再是瓶颈。IO 密集型应用的多线程收益就来自这里。


3 ~> CPU 密集型实测——GIL 确实把多线程打回原形

3.1 测试代码:暴力计算斐波那契额

importtime,threadingdeffib(n):"""纯计算,没有任何 IO 操作"""ifn<=1:returnnreturnfib(n-1)+fib(n-2)# 单线程基准start=time.perf_counter()for_inrange(4):fib(35)print(f"单线程串行耗时:{time.perf_counter()-start:.2f}秒")# 四个线程并发start=time.perf_counter()threads=[threading.Thread(target=fib,args=(35,))for_inrange(4)]fortinthreads:t.start()fortinthreads:t.join()print(f"四线程并发耗时:{time.perf_counter()-start:.2f}秒")

3.2 实测数据(i7-12700,8核16线程)

单线程串行耗时:12.84秒 四线程并发耗时:13.21秒(≈ 单线程,甚至还更慢!)

四个线程跑纯计算的斐波那契额,耗时和单线程串行基本一样——甚至因为上下文切换的开销稍慢一点。GIL 把四个线程压成了串行执行。

3.3 图解:CPU 密集型场景中 GIL 的行为

时间轴(每条横线代表一个线程占用GIL): 线程1:[████ 5ms ████][████ 5ms ████][████ 5ms ████]线程2:[████ 5ms ████][████ 5ms ████]线程3:[████ 5ms ████]线程4:[████ 5ms ████]↑ ↑ 不是并行,是快速切换——依然是每时刻一个线程。

4 ~> IO 密集型实测——线程数量和速度基本成正比

4.1 测试代码:并发网络请求

importtime,threading,requests URL="https://httpbin.org/delay/0.5"# 每个请求固定延迟 0.5 秒# 单线程start=time.perf_counter()for_inrange(8):requests.get(URL,timeout=5)print(f"单线程串行耗时:{time.perf_counter()-start:.2f}秒")# 八线程并发start=time.perf_counter()threads=[threading.Thread(target=lambda:requests.get(URL,timeout=5))for_inrange(8)]fortinthreads:t.start()fortinthreads:t.join()print(f"八线程并发耗时:{time.perf_counter()-start:.2f}秒")

4.2 实测数据

单线程串行耗时:4.12秒 (8 × 0.5秒 + 一点开销) 八线程并发耗时:1.24秒 (快了约 3.3 倍!)

八个线程同时发出请求,IO 阻塞时 GIL 被释放,其他线程能立刻获得解释器访问权继续发请求——在 IO 密度足够高的场景下,线程几乎可以同时并行工作。

4.3 图解:IO 密集型场景中 GIL 的行为

时间轴(线程在等 IO 时,GIL 释放给其他线程): 线程1:[GIL▕ 发请求 ▏ 等待响应 ▏收数据 ▕ 释放]线程2:[GIL▕ 等待]线程3:[GIL▕ 等待]线程4:[GIL▕ 等待]八个请求几乎同时发出 → 也几乎同时返回 → 总耗时接近0.5

GIL 只串行化字节码执行,不串行化 IO 操作。请求通过网络发出去之后,等待数据返回的过程中解释器什么都不做——这时候 GIL 释放给其他线程,让它们继续发出请求。


5 ~> 什么时候该用多线程、什么时候该切多进程

5.1 决策表

场景最佳方案原因
大量网络请求(爬虫、API 调用)多线程IO 阻塞时释放 GIL,多线程收益显著
大量磁盘读写(日志处理)多线程同 IO 密集,但注意文件系统性能瓶颈
纯 Python 计算(数论、字符串处理)多进程GIL 限制,多线程≈单线程
NumPy/Pandas 数据处理多线程底层 C 代码释放 GIL
数据库查询多线程等待数据库响应时 GIL 已释放
GUI 事件 + 后台计算多线程主线程处理 UI,后台线程做计算

5.2 一个真实的踩坑经历

2019 年给团队写了一个数据清洗脚本——从 Redis 批量读到 Pandas DataFrame 里做清洗,再写到 MySQL。一开始写的多线程版本,8 个线程并发处理,结果单机处理速度和单线程相差无几。

排查发现瓶颈不在 IO,而在 Pandas 的 DataFrame 合并操作——这部分被 GIL 锁住,八个线程几乎串行。把任务拷进一个进程池(4 个 worker),每个 worker 里全跑 Pandas 操作——处理时间从 12 分钟降到了 4 分钟。

教训:先测瓶颈在哪再选方案。不要听说"GIL 限制多线程"就否定一切多线程设计,也不要迷信"多线程一定比单线程快"。


思考 && 总结

GIL 不是洪水猛兽——它是一把只在特定场景中成为瓶颈的锁。

  1. CPU 密集型任务(纯 Python 计算):GIL 确实限制多线程并行。改成multiprocessing,或者切到 C 扩展(Cython/Numba)绕过 GIL。
  2. IO 密集型任务(网络、磁盘、数据库):多线程收益显著。IO 等待期间 GIL 主动释放,其他线程获得执行机会,吞吐量接近线性增长。
  3. NumPy/Pandas 场景:底层 C 实现大量释放 GIL,多线程可跑满多核。

下篇我们深入到绕过 GIL 的三种实战方案——多进程池、C 扩展、异步编程(asyncio),并给一个完整的决策树。


结尾

各位小伙伴,上篇到这里就结束了。源码骑士再次感谢您的阅读!

源码骑士 — 源码级拆解,从底层看透技术

👀关注:跟博主一起从源码视角深耕底层原理

❤️点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬评论:分享你的经历或疑问,一起交流

🔄一键四连:别忘了给博主一键四连!今日源码拆解达成!

🗡️寄语:先测再改,才不当改量的大冤种。

结语:GIL 是 Python 的一道坎,也是面试的一道试金石。上篇告诉你"什么时候它真的是瓶颈",下篇讲"怎么绕过它"。一键四连别忘了!

http://www.rkmt.cn/news/1525792.html

相关文章:

  • CVE-2026-5027全链路攻防深度剖析:Langflow未认证远程代码执行漏洞原理、在野利用与AI低代码安全体系建设
  • 3分钟解决TranslucentTB的Microsoft.VCLibs.140.00缺失错误:完整配置指南
  • 从淘汰到重生:一个开源项目如何让150+款老Mac焕发新生
  • 网页时光机:3个技巧让你永远告别404错误,轻松找回消失的网页内容
  • PC端微信QQ防撤回补丁:完整保留聊天记录的技术方案
  • 《Python程序设计》实验4报告
  • 破局进口垄断,深耕本土市场|膜利法则以全产业链实力,重塑国产汽车膜新格局 - 资讯速览
  • UniApp消息推送选型实战:UniPush 2.0 vs 极光推送,从成本到送达率的深度对比
  • 3个步骤掌握Maid:在手机上免费运行AI大模型的终极指南
  • 终极方案:3步彻底解决Cursor自动更新导致试用重置问题
  • 广州擅长职务侵占罪刑事律师推荐榜(2026):涉企经济犯罪辩护深度解析 - 互联网科技品牌测评
  • 基于ML307R Cat.1 4G模块的ESP32智能硬件双网络架构设计与实现
  • 2026年6月最新版来宾正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • OpenGL基础
  • 5分钟掌握猫抓Cat-Catch:浏览器资源嗅探工具的完整使用指南
  • MPC8245 DUART深度解析:从异步串口原理到寄存器编程实战
  • 鸿蒙原生应用实战(五)ArkUI 图片拼接/长图生成:多图合并 + Canvas 绘制 + 导出分享
  • 终极BT下载加速指南:如何用trackerslist项目彻底告别龟速下载
  • 2026年6月最新版莱芜正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 存算一体芯片软件双模式:单字符驱动网络(普通CPU也能跑)
  • AIOps 智能容量预测与弹性伸缩联动:从经验估算到数据驱动,云资源的成本与性能平衡
  • 深入解析MPC8309 eSDHC中断机制:SDIO通信稳定性的关键
  • 2026年6月最新版酒泉正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 消费级柔性机器人公司SoulX获融资,首款产品MoYa将带来家庭智能关护新体验!
  • 18-生成器不只是省内存(上)-yield的状态机模型与帧暂停
  • 告别命令行烦恼:将SillyTavern打造成真正的桌面应用,享受一键启动的AI聊天体验
  • 3步搞定洛雪音乐音源配置:免费获取全网无损音乐的终极方案
  • 高压型侧装式磁翻板液位计UXJC-1260-1-A-2
  • FDC故障检测规则设计:从人工经验到AI自动学习
  • MPC8306定时器模块详解:RTC、PIT与GTM的设计原理与工程实践