Python - GIL
Cpu bound
单线程 - 2.555494917
1 | import time |
多线程 - 2.477930167
1 | import time |
GIL
引用计数导致的 race condition
- Python 线程,封装了底层的 OS 线程,完全受 OS 管理,与 C++ 线程本质上是不同的抽象,但底层是一样的
- GIL 是最流行的 Python 解释器 CPython 中的一个技术术语 - 全局解释器锁,本质上是类似 OS 的 Mutex
- 每个 Python 线程,在 CPython 解释器中执行时,都会锁住自己的线程,阻止其它线程执行
- CPython 轮流执行 Python 线程 - 交错执行,模拟并行
- CPython 使用引用计数来管理内存
- 所有 Python 脚本中创建的实例,都会有一个引用计数,记录有多少指针指向它
- 当引用计数为 0 时,自动释放内存
1 | import sys |
- 如果没有 GIL,可能会有两个 Python 线程同时引用了 a,造成引用计数的 race condition - 最终只增加 1
- 内存被污染
- 第一个 Python 线程结束时,会把引用技术减少 1,此时已达到释放内存的条件
- 第二个 Python 线程再尝试访问 a 时,就找不到有效的内存了
CPython 引入 GIL 的主要原因
- 为了规避类似内存管理的复杂 race condition 问题
- CPython 大量使用了 C 语言库,大部分都不是原生线程安全的
工作过程
- Python 线程轮流执行,每个 Python 线程在开始执行前,都会锁住 GIL,以阻止其它 Python 线程执行
- 每个 Python 线程在执行完一段后(如遇到 IO 阻塞),会释放 GIL ,允许别的线程开始利用资源
- CPython 解释器会轮询检查 GIL 的锁住情况,每隔一段时间,CPython 会强制当前 Python线程去释放 GIL
每个 Python 线程都是一个循环的封装 - 周期性检查
1 | for (;;) { |
线程安全
GIL ≠ 线程安全,因为可以抢占,不保证原子性
- GIL 在同一个时刻,仅允许一个 Python 线程运行,但不会保证执行单元的原子性
- 为了避免 Python 线程饿死,还提供了 check interval 的抢占机制
n += 1
并非线程安全,字节码对应多个指令(与 JVM Class 非常类似)- 多个指令是有可能在中间被打断的
GIL 的设计,是为了方便 CPython 解释器层面的编写者,而非 Python 应用层面的程序员 - 用 Lock 等工具来保证线程安全
1 | import threading |
绕过
- Python 如果不需要 CPython 解释器来执行,将不再受 GIL 限制
- 很多高性能的应用场景都已经有了大量 C 实现的 Python 库,不受 GIL 影响 - 如 NumPy 的矩阵运算
- 在大部分应用情况下,不需要过多考虑 GIL
- 如果多线程计算成为性能瓶颈,一般都已经有 Python 库来解决这个问题
- 思路
- 绕过 CPython,使用 JPython 等其它实现
- 将关键性能代码在 C++/Rust 中实现(不受 GIL 限制),然后提供给 Python 的调用接口
多线程
- GIL 是只在同一时刻,程序只能有一个线程运行
- Python 多线程 - 多个线程交替运行(伪并行),具体到某一时刻,仍然只有 1 个线程在运行,并不是真正的多线程并行
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.