Python GIL详解
GIL是什么
GIL(Global Interpreter Lock,全局解释器锁) 是 CPython 解释器中的一个全局锁:
- 在同一时刻,只有一个线程可以执行 Python 字节码;
- 即使你开了很多线程,在同一个进程里,也只能有一个线程真正在跑 Python 代码(其它在等锁)。
这里需要注意的是,Python有多种实现例如:CPython、PyPy、Jython等,我们通常所指的python实际上指的是Cpython。
- 先说清楚:它们都是Python 的不同实现
Python 是一门语言(语法/规范)
CPython、PyPy、Jython、IronPython是这门语言的不同实现
类似于:
- Java 语言有 HotSpot、OpenJ9 等不同虚拟机
- JS 有 V8、SpiderMonkey、JavaScriptCore 等不同引擎
只要符合 Python 语言规范,就都可以叫“Python 实现”。
CPython:你现在用的“默认Python”
“官网 python.org 下回来、命令行里敲 python 的,大概率就是CPython。”
特点:
- 用C语言写的解释器,所以叫
C-Python - 这是 官方/标准实现,生态最好
- 有 GIL(Global Interpreter Lock)
- 大部分扩展库(如 numpy, pandas, django, ortools)都是针对 CPython 做的
优点:
- 稳定、兼容性最强、库最多
- 文档、社区、教程几乎都以
CPython为基准
缺点:
- 有 GIL → 纯 Python 多线程不能真正多核跑 CPU 密集任务
- 性能不是特别极致(但够用 + 可使用C拓展补强)
几乎所有人默认都在用 CPython,你也是。
而GIL 是 CPython 实现层面的机制,不是 Python 语言本身的语法特性;PyPy、Jython、IronPython 等实现可以有不同的策略(有的就没有传统意义的 GIL,或者用别的机制。
为什么要引入GIL?
早期的设计目标主要是:
在 多线程 + 引用计数 的内存管理模型下,简化解释器实现,降低并发 Bug(特别是内存管理方面)的复杂度。
CPython 使用 引用计数 管理对象生命周期,典型操作:
Py_INCREF(obj);
Py_DECREF(obj);
这些操作在多线程环境中会同时发生,如果 没有统一的锁 来保护这些共享对象的引用计数:
- 可能出现引用计数写冲突;
- 导致对象被错误释放或永不释放;
- 排查这种内存错误非常痛苦。
GIL 的核心目的:
- 让
CPython的内存管理(尤其是引用计数)在多线程环境下变得简单且安全; - 避免在所有对象、所有内部结构上都加各种细粒度锁(那样实现会极其复杂、性能难以预测、容易死锁)。
一句话概括:
GIL 是“用一个大锁,把解释器包起来”的暴力简单方案,用工程复杂度换取实现难度上的简单和安全。
- GIL 解决了什么问题?
- ✅ 避免了多线程下的对象引用计数竞争;
- ✅ 降低了 CPython 实现复杂度(少加很多锁、少很多细粒度同步);
- ✅ 让 C 扩展模块(在持有 GIL 情况下)可以默认认为自己是“线程安全的”(不需要自己再管 Python 层对象的并发访问)。
-
GIL 的优缺点
优点:
2.1 实现简单、安全
- 全局只有一个锁,解释器内部很多数据结构可以假设“不会被并发访问”;
- 内存管理(引用计数)实现成本低。
2.2 对 I/O 密集型任务友好
- I/O 操作(如网络、磁盘)通常会在系统调用时 释放 GIL;
- 即:当某个线程在等 I/O 时,别的线程可以获得 GIL 去执行;
- 所以多线程在 I/O 型场景(爬虫、网络服务客户端等)仍然可以获得不错的并发性能。
2.3 C 扩展库可以绕过 GIL 做多核计算
- 比如 numpy、ortools 等库,在内部使用 C/C++ 代码自行开线程并释放 GIL;
- Python 线程层面虽然有 GIL,但真正的计算发生在 C 层,可以充分利用多核。
缺点:(大家吐槽的点)
2.4 CPU 密集型 Python 代码,无法真正利用多核
- 同一进程里多个线程同时跑 Python 计算逻辑时,仍然是“轮流拿 GIL”;
- 多线程反而可能更慢(上下文切换 + GIL 争用)。
2.5 线程语义与多数语言不一致
- 别的语言(如 Java)里,“多线程 == 可利用多核并行执行计算”;
- 在
CPython里,“多线程 == 并发模型方便,但不等价于多核并行”。
2.6 增加了对性能调优的理解成本
- 你需要区分:
- CPU 密集 vs I/O 密集;
- Python 层代码 vs C/扩展层代码;
- 同样是线程,在不同场景下表现完全不同。
2.7 使得 Python 不适合作为高性能计算内核(纯 Python 实现)
- 真正要做大规模并行计算,通常要:
- 用 C/C++/Rust 实现核心算法;
- 或用 multiprocessing 多进程;
- 或用分布式计算框架(Ray、Dask、Spark 等)。
GIL 对 Python 线程、协程的影响
-
对线程(threading)的影响
核心:多个线程不能在多核上并行执行纯 Python 计算(CPU-bound)。
具体表现:
-
I/O 密集型任务:
- 比如开 100 个线程去发 HTTP 请求;
- 请求大部分时间在“等待网络”,这时候线程会在 I/O 调用时释放 GIL;
- 整体并发效果不错,多线程有价值。
-
CPU 密集型任务:
- 比如多个线程做大循环、计算素数、压缩、加密等纯 Python 运算;
- 它们会不断争抢 GIL,真正执行时“一个线程跑一会、切换、再另一个线程跑一会”,但任意时刻只有一个线程在执行 Python 代码;
- 多线程不仅不能提升速度,可能会变慢。
CPU 密集型:用 multiprocessing 或 C扩展绕过 GIL;
I/O 密集型:用多线程很合适。 -
-
对协程(asyncio、async/await)的影响
协程是 “单线程内的并发调度机制”:
- 本质上是一个线程 + 事件循环(event loop);
- 通过 await 主动“让出执行权”,让其他协程有机会运行;
- 底层仍然在一个 OS 线程里执行,所以: 协程本身并不绕开 GIL,还是受 GIL 约束的。
但协程的目标本来就不是多核并行,而是:
- 把大量 I/O 等待时间“塞进一个线程里”;
- 避免为每个 I/O 任务创建一个线程造成调度和内存开销;
所以:
- 对 I/O 密集型任务:协程 + 单线程完全可行,且效率高(不用创建大量线程);
- 对 CPU 密集型任务:协程一样没法同时跑多个任务,GIL 和线程一样会限制它。
如何绕过GIL?
python社区内关于是否需要保留GIL已经吵了很多年,虽然这两年也有no-GIL的尝试,但同时也会带来新的问题,总的来说是有推进但是也并不是短期内能够完全替代的。
严格来说在一个进程内GIL是无法绕过的,但是我们换一种思路去考虑这个问题:既然一个进程内无法绕过,那我们多起几个python进程不就解决了吗。
没错,实际上就是按照这种方式解决的,例如:multiprocessing和gunicorn。
浙公网安备 33010602011771号