Python GIL 原理深度解析:为何多线程无法利用多核性能
本文深入解析 Python 全局解释器锁(GIL)的工作原理及其对多线程性能的影响。GIL 导致 CPython 在同一时刻仅允许一个线程执行字节码,使得多线程在 CPU 密集型任务中沦为伪并行,无法发挥多核优势。文章详细阐述了 GIL 的加锁与释放机制、IO 密集型与计算密集型的差异,以及该设计的历史背景与简化实现的权衡。
1、说明
GIL(Global Interpreter Lock,全局解释器锁)的核心规定是:在 CPython 解释器中,同一时刻只能有一个线程被调度执行。
在 CPU 密集型任务或多线程代码场景中,GIL 往往成为显著的性能瓶颈。
由于 GIL 的存在,Python 的多线程实际上表现为“伪并行”,即线程交替执行而非真正同时运行。
需要注意的是,GIL 仅存在于 CPython 解释器中,其他实现如 Jython 或 IronPython 并不具备此限制。
2、原理
(1)线程调度机制:线程 1、2、3 等轮流获取执行权。每个线程在开始执行前必须获取 GIL,从而阻止其他线程同时运行;
当线程执行完一段指令后,会释放 GIL,允许其他等待的线程获取锁并开始利用 CPU 资源。
(2)GIL 的全局性导致竞争:即使线程 2 被调度在 CPU2 上执行,它仍需等待在 CPU1 上运行的线程 1 释放 GIL(因为 GIL 是全局共享资源)。
(3)锁的释放条件:如果线程 1 因 I/O 阻塞主动让出 GIL,线程 2 将直接获得 GIL;但如果线程 1 是因为 timer ticks 计数满 100 ticks(大致对应 1000 个字节码指令)而被动让出 GIL,此时线程 1 和线程 2 将公平竞争获取锁。
(4)优先级问题:在 Python 2.x 版本中,线程 1 不会动态调整自身优先级,因此在下一次调度周期中,线程 1 极大概率再次被选中。在多个这样的调度周期内,线程 2 只能被动等待,看着线程 1 在 CPU1 上持续执行。
(5)极端情况:若线程 1 使用 `while True` 在 CPU1 上持续进行计算且不进行 I/O 操作,将导致“一核有难,八核围观”的局面,其他核心无法有效参与计算。
知识点扩展:
GIL 设计理念与限制
Python 代码的执行由 Python 虚拟机(即解释器主循环,特指 CPython 版本)控制。Python 在设计之初便假定解释器主循环中同一时刻仅有一个线程运行。对虚拟机访问的控制由全局解释锁(GIL)管理,正是这个锁确保了同一时刻只有一个线程能够运行。
当调用外部代码(如 C 或 C++ 扩展函数)时,GIL 会被锁定,直到该函数执行完毕(由于期间没有 Python 字节码运行,因此不会发生线程切换)。
Python 中使用的均为操作系统级别的线程,Linux 下使用 pthread,Windows 下使用其原生线程机制。
从上述概述可直观看出,Python 在同一时刻只能运行一个线程。在多线程场景下,只有获取到全局解释器锁的线程才能运行。由于 GIL 只有一个,即使在多核硬件环境下,Python 也只能发挥单核的计算能力。
这似乎表明 Python 性能不足,GIL 直接导致 CPython 无法利用物理多核性能进行加速。那么为何保留此设计?回顾 Guido van Rossum 创造 Python 的上世纪 90 年代,多核 CPU 尚属罕见概念。如今硬件发展迅猛,程序需充分利用 CPU 性能以避免被淘汰,Python 亦需面对这一挑战。
上述内容主要阐述了该设计的劣势,接下来讨论其优势。
GIL 的设计简化了 CPython 的实现,使得对象模型(包括字典等关键内建类型)可以隐式地并发访问。锁定全局解释器使得实现多线程支持变得相对容易,但这确实牺牲了多处理器主机的并行计算能力。
然而,无论是标准库还是第三方扩展模块,均被设计为在进行密集计算任务时释放 GIL。此外,在执行 I/O 操作时,GIL 总是会被释放。对于所有涉及内置操作系统 C 代码的程序,GIL 会在 I/O 调用前被释放,以允许其他线程在等待 I/O 时继续运行。如果是纯计算程序且无 I/O 操作,解释器会每隔 100 次操作或每隔一定时间(如 15ms)释放 GIL。
由此可以理解,I/O 密集型的 Python 程序比计算密集型程序更能从多线程环境中获益。
到此这篇关于 Python 中 GIL 的原理及用法总结的文章就介绍到这了,更多相关 Python 中 GIL 的原理内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









