文章目录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-127008核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 到底是什么——一句话能解释清楚GILGlobal 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 操作网络读写、文件读写、sleepGIL 会在 IO 阻塞发生前主动释放。这就是多线程在 IO 密集场景下依然高效的原因——线程在等网路响应的时候不锁着解释器。2.2 什么时候 GIL 会被释放操作类型GIL 是否释放说明纯 CPU 计算for i in range(10^9)❌ 不释放只释放时间片只有 5ms 切换没有主动放弃time.sleep(1)✅ 释放阻塞出主动释放socket.recv()✅ 释放IO 操作底层 C 库释放 GILfile.read()✅ 释放同 IO 操作numpy.dot(A, B)✅ 释放大量 NumPy 运算在 C 层面完成C 代码释放 GIL一把锁如果能在任务阻塞时释放那它在阻塞密集的场景中就不再是瓶颈。IO 密集型应用的多线程收益就来自这里。3 ~ CPU 密集型实测——GIL 确实把多线程打回原形3.1 测试代码暴力计算斐波那契额importtime,threadingdeffib(n):纯计算没有任何 IO 操作ifn1:returnnreturnfib(n-1)fib(n-2)# 单线程基准starttime.perf_counter()for_inrange(4):fib(35)print(f单线程串行耗时{time.perf_counter()-start:.2f}秒)# 四个线程并发starttime.perf_counter()threads[threading.Thread(targetfib,args(35,))for_inrange(4)]fortinthreads:t.start()fortinthreads:t.join()print(f四线程并发耗时{time.perf_counter()-start:.2f}秒)3.2 实测数据i7-127008核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 URLhttps://httpbin.org/delay/0.5# 每个请求固定延迟 0.5 秒# 单线程starttime.perf_counter()for_inrange(8):requests.get(URL,timeout5)print(f单线程串行耗时{time.perf_counter()-start:.2f}秒)# 八线程并发starttime.perf_counter()threads[threading.Thread(targetlambda:requests.get(URL,timeout5))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 不是洪水猛兽——它是一把只在特定场景中成为瓶颈的锁。CPU 密集型任务纯 Python 计算GIL 确实限制多线程并行。改成multiprocessing或者切到 C 扩展Cython/Numba绕过 GIL。IO 密集型任务网络、磁盘、数据库多线程收益显著。IO 等待期间 GIL 主动释放其他线程获得执行机会吞吐量接近线性增长。NumPy/Pandas 场景底层 C 实现大量释放 GIL多线程可跑满多核。下篇我们深入到绕过 GIL 的三种实战方案——多进程池、C 扩展、异步编程asyncio并给一个完整的决策树。结尾各位小伙伴上篇到这里就结束了。源码骑士再次感谢您的阅读源码骑士 — 源码级拆解从底层看透技术关注跟博主一起从源码视角深耕底层原理❤️点赞让优质内容被更多人看见⭐收藏核心知识点存好随用随查评论分享你的经历或疑问一起交流一键四连别忘了给博主一键四连今日源码拆解达成️寄语先测再改才不当改量的大冤种。结语GIL 是 Python 的一道坎也是面试的一道试金石。上篇告诉你什么时候它真的是瓶颈下篇讲怎么绕过它。一键四连别忘了