为什么CPython要有GIL
一、为什么 CPython 要有 GIL?(追问点)
1.1 历史原因
CPython 诞生于 1991 年,当时多核 CPU 还不存在。GIL 是一个简单高效的设计决策,不是”错误”。
1.2 GIL 解决的两个核心问题
① 保护 CPython 对象的内存安全
# Python 中一切皆对象,引用计数是核心机制
a = [1, 2, 3] # 创建列表对象,引用计数 = 1
b = a # 引用计数 = 2
del a # 引用计数 = 1
通俗比喻:引用计数就像一个共享白板上的计数器——有人要看这块白板,计数器 +1;不看了,计数器 -1;减到 0 就把白板扔掉。
问题来了:如果两个人同时看到计数器是 1,都去 -1,结果变成 -1(或者一个人 -1 变成 0 白板被扔了,另一个人还试图在上面写字)——内存泄漏或者野指针就来了。这就是竞态条件(race condition)。
GIL 的解决方案简单粗暴:同一时刻只让一个人碰计数器,其他人排队。虽然笨,但管用。
② 让 C 扩展库更容易编写
大量 Python 生态的 C 扩展库(NumPy、Pandas)都假设”同一时刻只有一个线程执行 Python 代码”。
通俗比喻:就像你租了一间厨房(写 C 扩展库),房东(GIL)保证”同一时刻只有你一个人用厨房”。你可以放心地把菜刀随便放、锅不盖盖子——反正没人会碰你的东西。如果房东突然说”以后厨房共用,自己锁柜子”,那你所有厨具的摆放方式都得重来——整个生态都得改。
1.3 简答:为什么 Python 有 GIL?
GIL 是 CPython 的历史设计选择,主要两个原因:1)保护引用计数机制的线程安全,防止多线程并发修改对象导致内存问题;2)简化 C 扩展库的开发,让第三方库不需要考虑线程安全。代价是多线程无法利用多核 CPU 并行执行 Python 代码。
速记卡(面试闪卡)
Q1:一句话讲清「为什么CPython要有GIL」到底是什么?
A:GIL 是 CPython 的全局锁,同一时刻只放一个线程跑 Python 代码;换来引用计数线程安全和生态简单,代价是多线程吃不满多核。
Q2:历史原因——多核还没出生 —— 怎么理解?
A:GIL 诞生于 1991 年,那时多核 CPU 还不存在,它是简单高效的设计而非错误。像老式单车道马路,当年车少够用,如今车多了才显得堵。
Q3:保护引用计数的线程安全 —— 怎么理解?
A:Python 一切皆对象,靠引用计数决定何时回收;并发改计数会出竞态(两人同时 -1 变 -1 或野指针)。GIL 让”同一时刻只有一个人碰计数器”,简单粗暴但管用。像公共白板计数器,一次只许一人改,避免数错。
Q4:让 C 扩展库更好写 —— 怎么理解?
A:NumPy、Pandas 等 C 扩展都假设”同一时刻只有一个线程跑 Python 代码”,所以随便放厨具不锁柜子。若去掉 GIL,整个生态得重写线程安全逻辑。像房东保证厨房独享,你才敢乱放刀。
Q5:代价与绕过思路 —— 怎么理解?
A:代价是 CPU 密集型多线程无法利用多核——一个线程跑时其他干等。但 I/O 密集型不受影响(等待时释放 GIL);真要并行算多用多进程或 C 扩展释放 GIL。像一顿饭只能一人掌勺,但洗菜切菜可并行外包。
Q6:核心速记主线有哪些?
-
GIL 是历史设计,多核时代前的简单选择
-
保护引用计数线程安全,防竞态
-
简化 C 扩展开发,生态无需写锁
-
代价:多线程吃不满多核;I/O 型无碍,计算型用多进程
口诀
A:GIL 全局一把锁,同刻只放一线程;
引用计数得保护,扩展无需自己锁。
多核时代显短板,计算并行靠多进程;
IO 等待会释放,异步并发不耽搁。
相关链接
-
📋 目录:00-Python
-
📚 学习清单:八股文学习路线图
-
🔗 GIL是什么
-
🔗 怎么绕过GIL
-
🔗 进程vs线程vs协程 — 操作系统视角的并发基础
-
🔗 JS异步与事件循环 — Python asyncio vs JS 事件循环
-
🔗 进程vs线程vs协程 — GIL 是 CPython 线程的特殊限制