文章目录GIL 不是性能杀手下绕过 GIL 的三种方案——多进程、C扩展、asyncio 到底怎么选导入语1 ~ 方案一multiprocessing 多进程池——CPU密集场景的首选1.1 原理1.2 基础用法1.3 数据共享——多进程的代价1.4 我的经验2 ~ 方案二C 扩展——把瓶颈代码移出 GIL 的管辖区2.1 原理2.2 三种降低门槛的方式3 ~ 方案三asyncio 协程——单线程高并发3.1 原理3.2 示例并发请求3.3 asyncio 的局限4 ~ 终极决策树——你的场景该用哪个4.1 为什么这三种方案能并存思考 总结结尾GIL 不是性能杀手下绕过 GIL 的三种方案——多进程、C扩展、asyncio 到底怎么选文章简介上篇用实测数据验证了 GIL 只在 CPU 密集型场景中成为瓶颈本篇聚焦解决方案。逐一拆解三种绕过 GIL 的实战方案multiprocessing进程池——适合 CPU 密集的纯 Python 计算C 扩展Cython/Numba——在 C 层面运行计算密集代码自动释放 GILasyncio协程——单线程高并发处理 IO 密集任务。每种方案配有完整的可跑代码和适用场景分析结尾给一个决策树——拿到需求先看瓶颈类型再看数据共享需求最后选方案。穿插真实经历将数据处理服务从多线程切到多进程池性能提升 3 倍。 个人主页源码骑士❄专栏传送门《Android开发基础》《python基础课程》⭐️热衷从源码视角拆解技术底层原理将复杂架构讲得通俗易懂 源码骑士的简介5年Android Framework系统开发经验曾主导多项系统级性能优化专项技术栈覆盖Android系统全链路Binder/Handler/AMS/WMS/启动流程及Java后端全家桶Spring MyBatis Redis Oracle累计产出原创技术文章100篇文章以源码拆解为特色被读者评价为看一篇胜过啃一周文档导入语上篇讲清楚了 GIL 在什么场景下是瓶颈。这篇上干货——绕过 GIL 的三种方案。先声明一个前提没有最好的方案只有最适合你当前场景的方案。我见过很多人因为看了某篇推荐 asyncio 的文章就把所有东西写成协程——结果 CPU 密集计算部分不仅没变快还因为协程切换开销反而更慢了。三把刀各有用途——菜刀切菜、砍刀劈骨、水果刀削皮——你不可能只用一把刀做所有事。1 ~ 方案一multiprocessing多进程池——CPU密集场景的首选1.1 原理多进程不受 GIL 限制——因为每个进程有自己的独立 Python 解释器实例各带一把 GIL。四进程就是四个独立的 CPython各自运行各自的字节码。1.2 基础用法frommultiprocessingimportPoolimporttimedeffib(n):ifn1:returnnreturnfib(n-1)fib(n-2)if__name____main__:starttime.perf_counter()withPool(processes4)aspool:resultspool.map(fib,[35,35,35,35])print(f四进程池耗时{time.perf_counter()-start:.2f}秒)上篇四线程跑斐波那契额 13 秒。换成四进程——同样的计算耗时约 3.3 秒。几乎线性加速。1.3 数据共享——多进程的代价多进程的数据共享不免费。每个进程有自己的内存空间数据传递需要序列化pickle 跨进程 IPC 传输。如果输入输出数据量大几百 MB 的 DataFrame序列化开销可能比计算本身还大。共享内存或multiprocessing.Manager能缓解一部分但它们增加复杂度。适用判断计算开销远大于数据传输开销时用多进程。否则还不如单进程。1.4 我的经验之前那个数据处理服务——数据包很小每批 200KB raw 日志文本但计算极其重正则解析 聚合。切到 4-process Pool 之后跑量时间从 12 分钟降到 4 分钟。序列化开销约 1%可忽略。但如果数据包改成了 50MB 一张图片的压缩处理——单进程可能更快。2 ~ 方案二C 扩展——把瓶颈代码移出 GIL 的管辖区2.1 原理GIL 只锁 Python 字节码。如果你的代码跑在 C 层面可以在执行前调用Py_BEGIN_ALLOW_THREADS释放 GIL其他 Python 线程可以继续执行。NumPy、pandas 的大量核心运算是这样做的——这也是为什么你能在多线程中高效使用 NumPy。2.2 三种降低门槛的方式方式一Numba JIT 编译器——一行装饰器搞定fromnumbaimportjitimportmath,time,threadingjit(nopythonTrue,nogilTrue)# nopythonTrue: 启 JIT 编译; nogilTrue: 释放 GILdefheavyloop(n):result0foriinrange(n):resultmath.sqrt(i)returnresultnogilTrue告诉 Numba编译成原生代码后别忘了释放 GIL让其他 Python 线程也可以执行。多线程调用heavyloop时就能获得真正的并行。方式二Cython——写 Python 风格的 C 代码# heavy.pyxCython 文件cdef double heavyloop(intn)nogil:# nogil 声明这个函数不持有 GILcdef double result0cdefintiforiinrange(n):resultmath.sqrt(i)returnresultnogil关键字声明这个 Cython 函数执行时不持有 GIL。方式三直接用 C/Python API——不适合日常开发只适合写底层库这是 NumPy/Pandas 团队用的方法。一般你在 Python Web 应用里很少需要写 CPython C API知道这种做法存在就行。3 ~ 方案三asyncio协程——单线程高并发3.1 原理协程不是多线程——协程是单线程内的协作式多任务。一个协程在await时把控制权主动还给事件循环事件循环调度另一个协程执行。没有上下文切换开销没有 GIL 竞争可以轻松创建上万任务。只适用于 IO 密集——因为协程本身不利用多核。CPU 密集代码跑在协程里依然是单核。3.2 示例并发请求importasyncio,aiohttp,timeasyncdeffetch(session,url):asyncwithsession.get(url)asresp:returnawaitresp.text()asyncdefmain():asyncwithaiohttp.ClientSession()assession:tasks[fetch(session,fhttps://httpbin.org/delay/0.5)for_inrange(20)]resultsawaitasyncio.gather(*tasks)print(f并发 20 个请求完成)starttime.perf_counter()asyncio.run(main())print(f耗时{time.perf_counter()-start:.2f}秒)# 在 20 个并发请求的情况下耗时接近 0.5 秒而非串行的 10 秒3.3 asyncio 的局限整个调用链必须是异步的——一个同步库的调用会卡死事件循环。CPU 密集任务仍然会阻塞——可以用loop.run_in_executor()把重计算扔到线程池。调试比多线程还痛苦——没有堆栈帧Exception 信息少。4 ~ 终极决策树——你的场景该用哪个拿到一个性能优化任务 │ ├─ 瓶颈是 IO 流网络 / 磁盘 / 数据库查询 │ ├─ 并发量100需要快速上线 → 多线程 ✅ │ └─ 并发量1000全链路异步可用 → asyncio ✅ │ ├─ 瓶颈是 CPU 计算纯 Python 逻辑 │ ├─ 计算量不大代码简单 → 多进程池 ✅ │ ├─ 计算量中等能引入 C 扩展 → Numba / Cython ✅ │ └─ 库已经是 NumPy/Pandas → 多线程 ✅C 层面释放 GIL │ └─ 瓶颈不确定 └─ 先写最简版本 用 cProfile 跑一遍 → 从数据出发选方案别凭感觉4.1 为什么这三种方案能并存整个 Python 生态的设计是分层处理 GIL简单任务 → 多进程 —— 不需要改代码开箱即用性能敏感路径 → C 扩展 —— 把瓶颈移出 Python 字节码层高并发服务 → asyncio —— 单线程、低开销、十万级同时连接了解这三种方案之后你不再是因为 GIL 所以 Python 多线程没用的复读机而是能根据具体场景选工具的工程师。思考 总结三道门、三道锁总结一句话不要用什么方案先确定什么瓶颈。多进程解决 CPU 密集型——独立解释器无 GIL 竞争。代价是数据共享要序列化。C 扩展绕过 GIL——在计算前释放这把锁让其他线程干活。Numba 一行nogilTrue就能用。协程 asyncio解决高并发 IO——单线程、无锁、低开销。全链路异步是关键约束。记住决策树上那条不确定 → 先跑 profile——我踩过的坑里80% 的性能优化方向一开始都是错的。结尾GIL 上下篇到此完结。感谢耐心看到这里的你源码骑士 — 源码级拆解从底层看透技术关注跟博主一起从源码视角深耕底层原理❤️点赞让优质内容被更多人看见⭐收藏核心知识点存好随用随查评论分享你的经验或疑问一起交流一键四连别忘了给博主一键四连今日源码拆解达成️寄语先测后改是优化这个行当的第一条定律。结语GIL 是一个话题三种绕过去的方案一个决策树。下篇我们深入列表——lst.append(1)在 C 源码里到底做了什么。一键四连
12-GIL不是性能杀手(下)-绕过GIL的三种方案与决策树
文章目录GIL 不是性能杀手下绕过 GIL 的三种方案——多进程、C扩展、asyncio 到底怎么选导入语1 ~ 方案一multiprocessing 多进程池——CPU密集场景的首选1.1 原理1.2 基础用法1.3 数据共享——多进程的代价1.4 我的经验2 ~ 方案二C 扩展——把瓶颈代码移出 GIL 的管辖区2.1 原理2.2 三种降低门槛的方式3 ~ 方案三asyncio 协程——单线程高并发3.1 原理3.2 示例并发请求3.3 asyncio 的局限4 ~ 终极决策树——你的场景该用哪个4.1 为什么这三种方案能并存思考 总结结尾GIL 不是性能杀手下绕过 GIL 的三种方案——多进程、C扩展、asyncio 到底怎么选文章简介上篇用实测数据验证了 GIL 只在 CPU 密集型场景中成为瓶颈本篇聚焦解决方案。逐一拆解三种绕过 GIL 的实战方案multiprocessing进程池——适合 CPU 密集的纯 Python 计算C 扩展Cython/Numba——在 C 层面运行计算密集代码自动释放 GILasyncio协程——单线程高并发处理 IO 密集任务。每种方案配有完整的可跑代码和适用场景分析结尾给一个决策树——拿到需求先看瓶颈类型再看数据共享需求最后选方案。穿插真实经历将数据处理服务从多线程切到多进程池性能提升 3 倍。 个人主页源码骑士❄专栏传送门《Android开发基础》《python基础课程》⭐️热衷从源码视角拆解技术底层原理将复杂架构讲得通俗易懂 源码骑士的简介5年Android Framework系统开发经验曾主导多项系统级性能优化专项技术栈覆盖Android系统全链路Binder/Handler/AMS/WMS/启动流程及Java后端全家桶Spring MyBatis Redis Oracle累计产出原创技术文章100篇文章以源码拆解为特色被读者评价为看一篇胜过啃一周文档导入语上篇讲清楚了 GIL 在什么场景下是瓶颈。这篇上干货——绕过 GIL 的三种方案。先声明一个前提没有最好的方案只有最适合你当前场景的方案。我见过很多人因为看了某篇推荐 asyncio 的文章就把所有东西写成协程——结果 CPU 密集计算部分不仅没变快还因为协程切换开销反而更慢了。三把刀各有用途——菜刀切菜、砍刀劈骨、水果刀削皮——你不可能只用一把刀做所有事。1 ~ 方案一multiprocessing多进程池——CPU密集场景的首选1.1 原理多进程不受 GIL 限制——因为每个进程有自己的独立 Python 解释器实例各带一把 GIL。四进程就是四个独立的 CPython各自运行各自的字节码。1.2 基础用法frommultiprocessingimportPoolimporttimedeffib(n):ifn1:returnnreturnfib(n-1)fib(n-2)if__name____main__:starttime.perf_counter()withPool(processes4)aspool:resultspool.map(fib,[35,35,35,35])print(f四进程池耗时{time.perf_counter()-start:.2f}秒)上篇四线程跑斐波那契额 13 秒。换成四进程——同样的计算耗时约 3.3 秒。几乎线性加速。1.3 数据共享——多进程的代价多进程的数据共享不免费。每个进程有自己的内存空间数据传递需要序列化pickle 跨进程 IPC 传输。如果输入输出数据量大几百 MB 的 DataFrame序列化开销可能比计算本身还大。共享内存或multiprocessing.Manager能缓解一部分但它们增加复杂度。适用判断计算开销远大于数据传输开销时用多进程。否则还不如单进程。1.4 我的经验之前那个数据处理服务——数据包很小每批 200KB raw 日志文本但计算极其重正则解析 聚合。切到 4-process Pool 之后跑量时间从 12 分钟降到 4 分钟。序列化开销约 1%可忽略。但如果数据包改成了 50MB 一张图片的压缩处理——单进程可能更快。2 ~ 方案二C 扩展——把瓶颈代码移出 GIL 的管辖区2.1 原理GIL 只锁 Python 字节码。如果你的代码跑在 C 层面可以在执行前调用Py_BEGIN_ALLOW_THREADS释放 GIL其他 Python 线程可以继续执行。NumPy、pandas 的大量核心运算是这样做的——这也是为什么你能在多线程中高效使用 NumPy。2.2 三种降低门槛的方式方式一Numba JIT 编译器——一行装饰器搞定fromnumbaimportjitimportmath,time,threadingjit(nopythonTrue,nogilTrue)# nopythonTrue: 启 JIT 编译; nogilTrue: 释放 GILdefheavyloop(n):result0foriinrange(n):resultmath.sqrt(i)returnresultnogilTrue告诉 Numba编译成原生代码后别忘了释放 GIL让其他 Python 线程也可以执行。多线程调用heavyloop时就能获得真正的并行。方式二Cython——写 Python 风格的 C 代码# heavy.pyxCython 文件cdef double heavyloop(intn)nogil:# nogil 声明这个函数不持有 GILcdef double result0cdefintiforiinrange(n):resultmath.sqrt(i)returnresultnogil关键字声明这个 Cython 函数执行时不持有 GIL。方式三直接用 C/Python API——不适合日常开发只适合写底层库这是 NumPy/Pandas 团队用的方法。一般你在 Python Web 应用里很少需要写 CPython C API知道这种做法存在就行。3 ~ 方案三asyncio协程——单线程高并发3.1 原理协程不是多线程——协程是单线程内的协作式多任务。一个协程在await时把控制权主动还给事件循环事件循环调度另一个协程执行。没有上下文切换开销没有 GIL 竞争可以轻松创建上万任务。只适用于 IO 密集——因为协程本身不利用多核。CPU 密集代码跑在协程里依然是单核。3.2 示例并发请求importasyncio,aiohttp,timeasyncdeffetch(session,url):asyncwithsession.get(url)asresp:returnawaitresp.text()asyncdefmain():asyncwithaiohttp.ClientSession()assession:tasks[fetch(session,fhttps://httpbin.org/delay/0.5)for_inrange(20)]resultsawaitasyncio.gather(*tasks)print(f并发 20 个请求完成)starttime.perf_counter()asyncio.run(main())print(f耗时{time.perf_counter()-start:.2f}秒)# 在 20 个并发请求的情况下耗时接近 0.5 秒而非串行的 10 秒3.3 asyncio 的局限整个调用链必须是异步的——一个同步库的调用会卡死事件循环。CPU 密集任务仍然会阻塞——可以用loop.run_in_executor()把重计算扔到线程池。调试比多线程还痛苦——没有堆栈帧Exception 信息少。4 ~ 终极决策树——你的场景该用哪个拿到一个性能优化任务 │ ├─ 瓶颈是 IO 流网络 / 磁盘 / 数据库查询 │ ├─ 并发量100需要快速上线 → 多线程 ✅ │ └─ 并发量1000全链路异步可用 → asyncio ✅ │ ├─ 瓶颈是 CPU 计算纯 Python 逻辑 │ ├─ 计算量不大代码简单 → 多进程池 ✅ │ ├─ 计算量中等能引入 C 扩展 → Numba / Cython ✅ │ └─ 库已经是 NumPy/Pandas → 多线程 ✅C 层面释放 GIL │ └─ 瓶颈不确定 └─ 先写最简版本 用 cProfile 跑一遍 → 从数据出发选方案别凭感觉4.1 为什么这三种方案能并存整个 Python 生态的设计是分层处理 GIL简单任务 → 多进程 —— 不需要改代码开箱即用性能敏感路径 → C 扩展 —— 把瓶颈移出 Python 字节码层高并发服务 → asyncio —— 单线程、低开销、十万级同时连接了解这三种方案之后你不再是因为 GIL 所以 Python 多线程没用的复读机而是能根据具体场景选工具的工程师。思考 总结三道门、三道锁总结一句话不要用什么方案先确定什么瓶颈。多进程解决 CPU 密集型——独立解释器无 GIL 竞争。代价是数据共享要序列化。C 扩展绕过 GIL——在计算前释放这把锁让其他线程干活。Numba 一行nogilTrue就能用。协程 asyncio解决高并发 IO——单线程、无锁、低开销。全链路异步是关键约束。记住决策树上那条不确定 → 先跑 profile——我踩过的坑里80% 的性能优化方向一开始都是错的。结尾GIL 上下篇到此完结。感谢耐心看到这里的你源码骑士 — 源码级拆解从底层看透技术关注跟博主一起从源码视角深耕底层原理❤️点赞让优质内容被更多人看见⭐收藏核心知识点存好随用随查评论分享你的经验或疑问一起交流一键四连别忘了给博主一键四连今日源码拆解达成️寄语先测后改是优化这个行当的第一条定律。结语GIL 是一个话题三种绕过去的方案一个决策树。下篇我们深入列表——lst.append(1)在 C 源码里到底做了什么。一键四连