本篇文章介绍了 Python 全局解释锁的作用、释放 GIL 的时机、如何绕过 GIL 等,相信能够助力你通过面试官的拷问。
Python 的全局解释锁(Global Interpreter Lock, GIL)是 CPython 解释器(Python 官方实现)中的一个核心机制,用于确保同一时刻只有一个线程能够执行 Python 字节码。
1. GIL 是如何工作的?
GIL 本质上是一个互斥锁(mutex),确保在任何时刻只有一个线程在执行 Python 代码。当一个线程获取 GIL 后,其他线程必须等待该线程释放GIL后才能执行。即使在多核处理器上,同一时刻也只能有一个线程执行 Python 代码。
下面这张图,就是一个 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

2. GIL 的作用
GIL 的作用主要有三个:
- 简化内存管理。确保引用计数操作的原子性,防止内存泄漏和数据竞争。
Python 的内存管理。Python 主要通过引用计数(reference counting)进行内存管理。- 引用计数操作:每个 Python 对象都有一个引用计数器,记录有多少个变量引用这个对象。
- 当创建对象或者新的变量引用该对象时,引用计数+1;
- 当引用被删除或离开作用域时,引用计数-1;
- 当引用计数降为 0 时,对象被销毁,内存被释放。
- 引用计数的实现: 在CPython中,每个
PyObject结构体都包含一个整数字段ob_refcnt,用于记录引用计数。
- 引用计数操作:每个 Python 对象都有一个引用计数器,记录有多少个变量引用这个对象。
- 简化 CPython 解释器的实现。 GIL大大简化了 CPython 解释器的实现。不需要复杂的锁机制来保护共享数据结构。
- 保证C扩展的安全。让非线程安全的 C 库可以安全地在Python中使用。
3. 线程什么时候会释放 GIL?
- I/O操作:在执行I/O操作时,Python会释放GIL允许其他线程执行。
- 长时间计算:Python解释器会定期释放GIL(默认每100个字节码指令),给其他线程执行的机会。
- 内存安全保证:
- 当释放GIL时,确保当前线程不会执行任何可能修改共享Python对象的操作
- C 扩展在执行可能修改Python对象的操作前必须重新获取GIL
- 这确保了即使GIL被临时释放,内存管理操作仍然是安全的
4. 如何绕过 GIL?
- 使用多进程。通过
multiprocessing库可以启动多个Python进程,每个进程有自己的GIL。 - 使用非CPython解释器。如 Jython 和 IronPython 没有GIL。
- 使用 C 扩展。编写 C 扩展并在其中释放GIL。
- 使用并行计算库。如NumPy、Pandas等库的许多操作在C语言层面实现,会在计算期间释放GIL。
当然,如果你的程序需要高性能,那么选择 Python 语言可能是一个错误的决定。
5. 有了全局解释锁,为什么还需要线程间同步机制?
尽管Python有全局解释锁(GIL),但线程同步机制(如互斥锁、信号量、条件变量等)仍然是必要的,原因如下:
-
GIL的保护范围有限。GIL只保证Python字节码的执行是原子的,但无法保证更高级别操作的原子性:
- 字节码级别的原子性:GIL确保同一时刻只有一个线程执行Python字节码,但一个逻辑操作通常由多个字节码指令组成。
- 操作的非原子性:例如
counter += 1实际包含至少三个步骤:读取counter值、增加值、写回counter。在这些步骤之间,GIL可能被释放并切换到其他线程。
-
GIL的释放与切换。 Python解释器会在以下情况释放GIL:
- 定期释放:解释器每执行一定数量的字节码指令(默认100个),会自动释放GIL并允许其他线程运行。
- I/O操作时释放:执行I/O操作时,解释器会释放GIL,让其他线程有机会执行。
- 阻塞操作时释放:在某些阻塞操作(如等待网络响应)期间,GIL也会被释放。
下面是一个 GIL无法保证高级操作原子性的例子:
import threading
import time
counter = 0
def increment_counter(n):
global counter
for _ in range(n):
# 强制增加线程切换的可能性
current = counter
# 人为增加线程被切换的几率
time.sleep(0.000001) # 微小的延迟
counter = current + 1
# 使用更大的循环次数
iterations = 1000
t1 = threading.Thread(target=increment_counter, args=(iterations,))
t2 = threading.Thread(target=increment_counter, args=(iterations,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"预期结果: {iterations * 2}")
print(f"实际结果: {counter}")
6. 小结
GIL 的主要作用是简化 Python 的内存管理和 CPython 解释器的实现,以及确保 C 扩展的安全。
尽管 GIL 确保了同一时刻只有一个线程执行Python字节码,但它不能保证高级操作的原子性。Python的线程在执行过程中仍可能在字节码指令之间切换,导致数据不一致。因此,当多个线程访问共享资源时,适当的同步机制(如锁、信号量、条件变量等)仍然是必要的,以确保数据一致性和程序的正确行为。
今天的文章就到这里了。觉得有帮助的道友,不要忘记一键三连呐。
2120

被折叠的 条评论
为什么被折叠?



