▌ 技术引导
Python 的 GIL 是个绕不开的坎,你要是没研究过它的源码,很难真正理解它在多线程中的表现。我之前在做图像处理任务时,用多线程加速,结果 CPU 占用率卡在 100%,线程数越加,响应越慢,最后才发现是 GIL 在作怪。最近我深入看了 CPython 的源码,发现 GIL 的实现并不像大家想象的那样简单,而且在 3.11 版本后有明显的变化。比如,GIL 的释放逻辑从基于计数器改成了基于时间片轮转,这直接改变了并发行为。如果你在写高性能并发代码,特别是涉及 CPU 密集型任务,绝对不能忽略 GIL 的源码逻辑。我见过一些人用 threading 模块写线程池,结果性能不如多进程,就是没搞懂 GIL 的机制。这篇文章我直接开干,带你从源码角度看看 GIL 是怎么工作的,怎么影响你的代码,以及怎么绕过它。
▌ 技术参考
一 技术背景与核心概念
GIL(全局解释器锁)是 CPython 实现中的一个关键机制,它确保同一时间只有一个线程执行 Python 字节码。在 CPython 的源码中,GIL 被定义为一个全局的互斥锁,存放在 `PyThreadState` 结构体中。GIL 的存在主要是因为 CPython 对底层 C 代码的线程安全没有做完整封装,导致多个线程无法同时操作 Python 对象。在 2024 年的多个版本中,GIL 的释放策略有所优化,比如增加了更精细的锁调度策略。你可以在 `Python/ceval.c` 文件中找到 GIL 的核心控制逻辑,比如 `PyEval_RestoreThread` 和 `PyEval_SaveThread` 函数,它们负责线程状态的保存和恢复。2025 年 3.11 版本之后,GIL 的调度逻辑被重构,引入了时间片轮转的概念,这在 `thread.c` 中有体现。
二 具体操作方法或配置步骤
要观察 GIL 的行为,你可以使用 `threading` 模块创建多个线程,然后用 `psutil` 监控 CPU 使用率。在 CPython 源码中,`PyThreadState_Get` 函数用于获取当前线程的状态,而 `PyEval_SaveThread` 会主动释放 GIL,允许其他线程运行。例如,你可以在主线程中调用 `threading.Thread(target=cpu_intensive_task).start()`,并在任务中加入 `time.sleep(0.01)` 来模拟 CPU 占用。此外,在 `sys` 模块中,`getswitchinterval` 方法用于获取 GIL 的调度时间间隔,这个值在 3.11 版本后默认被调整为 0.005 秒。如果你在运行过程中发现线程无法并行执行,可以尝试修改 `PyThreadState` 中的 `interp` 字段,或者在 `thread.c` 中调整 `PyThreadState` 的初始化参数,但这不建议在生产环境中随意修改。
三 常见踩坑场景与避坑方案
在实际开发中,很多开发者误以为 Python 的多线程能完全取代多进程,但 GIL 会限制这一点。比如,你用 `threading` 写一个 CPU 密集型的线程池,结果发现所有线程都在串行执行,根本没并行。这时候你需要意识到,GIL 的存在会阻止多个线程同时运行 C 代码。常见的踩坑点还包括在 C 扩展中忘记释放 GIL,导致主线程被阻塞。我之前用 `numpy` 做数值计算时,由于其内部没释放 GIL,导致多线程性能完全不在线。解决方式有两个:一是改用多进程,二是通过 `Py_BEGIN_ALLOW_THREADS` 和 `Py_END_ALLOW_THREADS` 显式释放 GIL。前者适用于 C 扩展模块,后者适用于 Python 代码,这样可以确保在执行非 Python 代码时,GIL 会被释放,从而允许其他线程运行。
四 性能影响或效率对比
GIL 的存在对多线程性能有明显影响,特别是在 CPU 密集型任务上。我做过一次对比测试,在 2024 年 3.10 版本中,多个线程同时运行 CPU 计算任务时,总耗时几乎和单线程一样,因为 GIL 没有被释放。而在 3.11 版本中,由于调度逻辑的变化,多线程的性能提升明显,但仍然达不到多进程的水平。如果你的任务是 I/O 密集型的,比如网络请求或文件读写,多线程反而更有优势,因为 GIL 会在 I/O 操作时被释放。比如在使用 `asyncio` 或 `concurrent.futures.ThreadPoolExecutor` 时,线程数控制在 100 以内,同时结合 `time.sleep` 作为模拟 I/O,可以更高效利用资源。但如果你的代码里包含了大量 C 扩展,或者依赖第三方库没有释放 GIL,那多线程可能反而拖慢性能。
五 适用场景与局限性
GIL 的适用场景主要集中在 I/O 密集型任务,比如网络爬虫、异步处理、数据库访问等,这些情况下线程的切换和等待 I/O 的时间让 GIL 的影响变得不那么明显。但如果是 CPU 密集型任务,比如机器学习训练、图像处理、数学计算等,GIL 会成为性能的瓶颈。我之前在做图像处理任务时,用 `Pillow` 库,结果发现多线程没有带来预期的加速,因为 GIL 一直在阻塞。这时候必须切换到多进程,比如使用 `multiprocessing` 模块或者 `concurrent.futures.ProcessPoolExecutor`。另外,某些第三方库如 `numba` 或 `Cython` 提供了绕过 GIL 的方式,但需要特定的配置,比如设置 `numba` 的 `njit` 编译器标志,或者在 `Cython` 中使用 `nogil` 关键字。不过这些方法都有使用门槛,不是所有项目都能轻松实现。
六 替代方案或进阶技巧
如果你的任务无法通过多进程解决,或者你希望保持线程模型,可以考虑使用 `multiprocessing` 模块,它通过子进程的方式绕过 GIL 的限制。另外,你也可以使用 `PyPy` 或 `Jython` 这些替代解释器,它们对 GIL 的处理方式与 CPython 不同。比如,PyPy 在 2024 年的某些版本中已经去掉了 GIL,从而实现了真正的并行计算。不过这些替代方案可能会带来其他问题,比如 API 兼容性或性能差异。对于 CPython 开发者,可以尝试使用 `PyThreadState` 的 `interp` 字段来管理 GIL,或者在 C 扩展中使用 `Py_BEGIN_ALLOW_THREADS` 和 `Py_END_ALLOW_THREADS` 来手动释放 GIL。这些技巧虽然能提升性能,但需要对底层机制有深入了解,否则容易出错。
七 源码中 GIL 的生命周期管理
GIL 的生命周期管理主要体现在 `thread.c` 文件中,其中定义了线程的创建、销毁以及上下文切换。在 CPython 中,线程的初始化会调用 `PyThreadState_New` 函数,该函数会分配一个 `PyThreadState` 结构体,并将其与当前线程绑定。在执行字节码时,会通过 `PyThreadState_Get` 获取当前线程的状态,然后调用 `PyEval_SaveThread` 释放 GIL,这样其他线程才有机会运行。在 2025 年的版本中,`PyEval_SaveThread` 会根据当前线程的执行时间,决定是否允许其他线程抢占。如果你需要在源码中查看 GIL 的具体行为,可以打开 `Python/ceval.c` 文件,找到 `PyEval_EvalFrameEx` 函数,这里会处理字节码的执行,并在适当的时候释放 GIL。而 `PyEval_RestoreThread` 则用于恢复 GIL,确保当前线程可以继续执行字节码。
八 GIL 与线程调度的交互机制
在 CPython 中,线程调度是基于操作系统线程的,而 GIL 是 Python 层面的一个锁。线程调度器会根据 `PyThreadState` 的状态决定何时切换线程。比如,在 `threading` 模块中,`Thread` 类的 `start` 方法最终会调用 `PyThread_start_new_thread`,这个函数会在创建线程后设置 `PyThreadState` 的上下文。在 2024 年的版本中,线程切换的逻辑已经优化,通过 `PyThreadState` 结构体中的 `interp` 字段来管理锁的分配。如果你在开发 C 扩展模块,可以使用 `Py_BEGIN_ALLOW_THREADS` 和 `Py_END_ALLOW_THREADS` 来显式释放 GIL,这样就能让其他线程获取执行权。这些宏定义在 `Python/threads.c` 中,确保在执行非 Python 代码时,GIL 被释放,从而避免线程阻塞。
九 GIL 在多线程并发中的表现
我之前在做多线程爬虫项目时,发现线程数越多,性能反而越差。用 `threading` 创建 100 个线程访问网页,结果 CPU 占用率没超过 100%,但总耗时反而增加。这说明 GIL 在多个线程中被串行执行,导致实际并行程度不足。如果你的任务涉及大量 CPU 计算,比如图像滤镜处理,那么多线程可能不如多进程有效。不过对于 I/O 任务,比如网络请求或文件读取,多线程的效率会显著提升。比如,在使用 `requests` 库进行 HTTP 请求时,如果配合 `concurrent.futures.ThreadPoolExecutor`,你会发现多个线程可以同时发送请求,而不会因为 GIL 被阻塞。但要注意的是,如果 I/O 操作中包含大量的 CPU 计算,比如解析响应内容,那么 GIL 又会成为瓶颈。
十 GIL 的释放时机与上下文切换
GIL 的释放时机通常是通过 `PyEval_SaveThread` 函数触发的,这个函数会在执行字节码的中间阶段调用,比如在 `PyEval_EvalFrameEx` 函数中。在 2024 年的版本中,GIL 的释放逻辑被优化为时间片轮转,这意味着 GIL 会在一定时间后被释放,而不是每次调用 I/O 函数时。比如,如果你在 `threading` 中使用 `time.sleep`,这个函数会触发 GIL 的释放,让其他线程有机会运行。但如果你的代码中没有 I/O 调用,GIL 会一直保留在当前线程中,导致其他线程无法执行。在源码中,你可以在 `Python/ceval.c` 文件中找到 `PyEval_SaveThread` 的具体实现,它会将当前线程的 `PyThreadState` 保存到线程局部存储(TLS)中,并释放 GIL。而 `PyEval_RestoreThread` 会恢复 GIL,使当前线程可以继续执行 Python 代码。
十一 GIL 与第三方库的兼容性问题
很多第三方库在 2024 年的版本中已经对 GIL 进行了优化,但仍有部分库没有处理好。比如,`numpy` 在进行数组运算时,由于其底层是 C 代码,GIL 会一直被持有,导致多线程无法并行执行。我之前在用 `numpy` 做图像处理,结果发现线程数越多,性能越差,因为 GIL 没有被释放。为了避免这种情况,可以考虑使用 `numba` 的 `@njit` 装饰器,因为它会在编译时处理 GIL 的释放问题。另外,像 `asyncio` 这样的异步框架,会利用事件循环来调度任务,这样即使在多线程中,也能避免 GIL 的影响。不过对于某些库,比如 `Cython`,需要在代码中显式指定 `nogil` 关键字,才能让线程间真正并行运行。这些细节在源码中都有体现,但需要手动调整才能生效。
十二 GIL 的优化策略与版本差异
在 2024 年的多个 CPython 版本中,GIL 的优化策略存在差异。比如,在 3.11 版本中,GIL 的时间片轮转机制被引入,这样线程切换更高效,多线程的并发性能得到了提升。如果你在进行性能调优,可以查看 `PyThreadState` 结构体中的 `interp` 字段,它记录了当前线程的 GIL 状态。此外,你还可以通过设置环境变量如 `PYTHONTHREADS` 来控制线程数,或者使用 `sys.setswitchinterval` 来调整 GIL 的调度时间。这些配置项在 2024 年的版本中已被广泛使用,特别是当你需要优化多线程任务的调度效率时。不过要注意,这些配置项可能会影响其他线程的行为,需要谨慎测试。
十三 GIL 在 C 扩展中的处理方式
如果你在编写 C 扩展模块,那么熟悉 GIL 的处理方式非常重要。在 CPython 中,C 扩展默认会持有 GIL,直到执行完成。比如,`Py_BEGIN_ALLOW_THREADS` 和 `Py_END_ALLOW_THREADS` 这两个宏,会在执行 C 代码时释放 GIL,允许其他线程运行。在 2024 年的版本中,这些宏被优化为更高效的实现,减少对性能的影响。比如,`Py_BEGIN_ALLOW_THREADS` 会将 GIL 释放到操作系统层面,而 `Py_END_ALLOW_THREADS` 会在执行完 C 代码后重新获取 GIL。如果你在 C 扩展中使用了多个线程,或者需要多线程并行处理,那么这些宏是关键。此外,使用 `PyThreadState` 的 `interp` 字段,可以确保线程间的上下文切换不会因为 GIL 出现问题。
十四 GIL 与异步编程的结合方式
在 2024 年,异步编程与 GIL 的结合变得更加紧密。比如,`asyncio` 的事件循环会自动处理 GIL 的释放,确保在进行 I/O 操作时释放锁。这样,即使在多线程环境中,异步任务也能有效利用多核 CPU。如果你在使用 `asyncio` 编写高并发应用,可以尝试使用 `asyncio.gather` 来并行执行多个任务,而不需要显式地管理线程。此外,`concurrent.futures` 中的 `ThreadPoolExecutor` 也可以与 `asyncio` 结合使用,比如通过 `loop.run_in_executor` 来提交任务。这些方式都避免了 GIL 的影响,让代码在多线程中也能高效执行。
十五 替代方案的实际应用案例
在实际项目中,我见过一些人使用 `multiprocessing` 来绕过 GIL 的限制。比如,在一个机器学习项目中,使用 `ProcessPoolExecutor` 来并行处理数据,这样每个进程都有自己的 Python 解释器实例,不会受到 GIL 的影响。不过,这种方式也有缺点,比如进程间通信的成本较高,而且跨平台兼容性不如线程好。如果你的任务无法通过 `multiprocessing` 解决,可以考虑使用 `PyPy`,它在 2024 年的某些版本中已经去掉了 GIL,从而实现真正的并行。不过,`PyPy` 的性能和 Python 标准库的兼容性可能不如 CPython。另外,一些 C 扩展库如 `Cython` 或 `numba` 提供了 `nogil` 的编译选项,可以在代码中释放 GIL,从而提升性能。这些方法各有优劣,需要根据具体任务选择。
Python GIL源码解析:设计模式 | 全网最详细
Python 的 GIL 是个绕不开的坎,你要是没研究过它的源码,很难真正理解它在多线程中的表现。我之前在做图像处理任务时,用多线程加速,结果 CPU 占用率卡在 100%,线程数越加,响应越慢,最后才发现是 GIL 在作怪。最近我深入看了 CPython 的源码,发现 GIL 的实现并不像大家想象的那样简单,而且在 3.11 版本后有明
语言深潜AI1 次阅读
Related
延伸阅读

避坑 | SkyWalking镜像仓库(7分钟读完)DevOps实战 · 2026-07-10

12个VS Code settings.json团队规范,避坑必备VS Code指南 · 2026-07-10

建议收藏:VS Code Cursor 性能优化 | 老用户总结VS Code指南 · 2026-07-10

纯干货 | Angular Signals的17种样式方案前端工程 · 2026-07-14

保姆级教程 | PostgreSQL优化:性能优化实战数据库 · 2026-07-10

VS Code Copilot性能优化:4个快捷键速查 | 2026最新版VS Code指南 · 2026-07-13