为什么要锁定全局解释器?
- 2025-03-20 08:47:00
- admin 原创
- 41
问题描述:
Python 的全局解释器锁到底起什么作用?其他编译为字节码的语言是否采用类似的机制?
解决方案 1:
一般而言,对于任何线程安全问题,您都需要使用锁来保护内部数据结构。这可以通过各种粒度级别来实现。
您可以使用细粒度锁定,其中每个单独的结构都有自己的锁。
您可以使用粗粒度锁定,其中一个锁保护所有内容(GIL 方法)。
每种方法都有各自的优缺点。细粒度锁定允许更高的并行性 - 两个线程在不共享任何资源时可以并行执行。但是管理开销要大得多。对于每一行代码,您可能需要获取和释放多个锁。
粗粒度方法则相反。两个线程不能同时运行,但单个线程会运行得更快,因为它不需要做太多的记录。最终,这归结为单线程速度和并行性之间的权衡。
曾经有过几次尝试移除 Python 中的 GIL,但对于单线程机器来说,额外的开销通常太大。由于锁争用,某些情况下,即使在多处理器机器上,运行速度也会更慢。
编译为字节码的其他语言是否采用类似的机制?
它各不相同,可能不应将其视为语言属性,而应视为实现属性。例如,Jython 和 IronPython 等 Python 实现使用其底层 VM 的线程方法,而不是 GIL 方法。此外,Ruby 的下一个版本似乎正在引入GIL。
解决方案 2:
以下内容来自官方的 Python/C API 参考手册:
Python 解释器并非完全线程安全。为了支持多线程 Python 程序,当前线程必须持有一个全局锁,然后才能安全地访问 Python 对象。如果没有锁,即使是最简单的操作也可能导致多线程程序出现问题:例如,当两个线程同时增加同一对象的引用计数时,引用计数最终可能只会增加一次,而不是两次。
因此,存在这样的规则:只有获取了全局解释器锁的线程才可以操作 Python 对象或调用 Python/C API 函数。为了支持多线程 Python 程序,解释器会定期释放并重新获取锁 - 默认情况下,每 100 条字节码指令(可以使用 sys.setcheckinterval() 进行更改)。在可能阻塞 I/O 操作(例如读取或写入文件)时,也会释放和重新获取锁,以便其他线程可以在请求 I/O 的线程等待 I/O 操作完成时运行。
我认为它很好地概括了这个问题。
解决方案 3:
全局解释器锁是一个大型互斥锁,可防止引用计数器被破坏。如果您编写的是纯 Python 代码,这一切都在幕后发生,但如果您将 Python 嵌入到 C 中,那么您可能必须明确获取/释放锁。
此机制与 Python 编译为字节码无关。Java 不需要它。事实上, Jython(python 编译为 jvm)甚至不需要它。
另请参阅这个问题
解决方案 4:
Python 和 perl 5 一样,从一开始设计时就不是线程安全的。线程是事后才添加的,因此使用全局解释器锁来保持互斥,即在给定时间内只有一个线程在解释器内部执行代码。
解释器本身通过不时循环锁来协同执行各个 Python 线程的多任务。
当您使用 C 与 Python 对话时,如果其他 Python 线程处于活动状态,则需要自己获取锁以“选择加入”此协议,并确保在您背后不会发生任何不安全的事情。
其他具有单线程传统、后来演变为多线程系统的系统通常具有某种此类机制。例如,Linux 内核从其早期的 SMP 时代就具有“大内核锁”。随着时间的推移,随着多线程性能逐渐成为一个问题,人们倾向于尝试将这些类型的锁分解成更小的部分,或尽可能用无锁算法和数据结构替换它们,以最大限度地提高吞吐量。
解决方案 5:
关于你的第二个问题,并非所有脚本语言都使用这个,但这只会削弱它们的功能。例如,Ruby 中的线程是绿色的,不是原生的。
在 Python 中,线程是原生的,GIL 仅阻止它们在不同的核心上运行。
在 Perl 中,线程更糟糕。它们只是复制整个解释器,远不如 Python 那么好用。
扫码咨询,免费领取项目管理大礼包!