CPython靠GIL维系单线程性能与生态 锁死多核原生算力加速

【python】天使还是魔鬼?GIL的前世今生。一期视频全面了解GIL! — 码农高天

码农高天

GIL通过全局互斥执行消除了引用计数的并发冲突并降低了C扩展门槛,但也导致解释器无法在多核时代实现原生多线程并行计算,迫使密集任务依赖多进程规避性能瓶颈。

AI-assisted summary of 【python】天使还是魔鬼?GIL的前世今生。一期视频全面了解GIL! — original source. Check the original for full context.

Source published:

Key ideas

  • GIL 的诞生本质上是为了解决 CPython 内存管理中“引用计数”在多线程环境下的非原子性并发竞争问题。 (2:30)
  • GIL 方案长期得以保留的核心原因在于其架构实现简单、天然避免了死锁,并且最大程度保障了单线程性能与 C 扩展生态的开发便利性。 (7:15)
  • 诞生于单核时代的 GIL 在如今多核 CPU 普及的背景下成为瓶颈,导致 Python 无法利用原生多线程有效加速 CPU 密集型任务。 (9:30)
  • 目前开发者规避 GIL 限制的主要方案包括使用 multiprocessing 模块进行多进程并发、在底层 C 扩展中实现多线程,或选用 Jython 等无 GIL 替代解释器。 (10:45)

Detailed notes

## Background 该视频由技术博主“码农高天”于 2023 年 10 月 16 日发布在 YouTube 平台上。视频旨在系统梳理 Python 社区中讨论度极高且常被误解的 GIL(全局解释器锁),从其诞生历史、技术现状到优缺点进行全方位解析,帮助开发者彻底解开对 GIL 的迷茫。

## TL;DR 内容从操作系统底层的线程、进程及竞争冒险等基础概念切入,深入剖析了 CPython 采用引用计数进行内存管理时面临的并发危机,进而解释了引入全局解释器锁(GIL)的根本原因。随后,作者从“天使”的一面论证了 GIL 具备架构极简、杜绝死锁、保障单线程高效运行以及大幅降低 C 扩展开发门槛等巨大红利,说明了其长期无法被轻易移除的技术原因。最后,作者直面 GIL 在现代多核硬件下导致原生线程无法实现 CPU 密集型任务并行的“恶魔”一面,并为开发者梳理了多进程(multiprocessing)、底层 C 扩展多线程以及选用 Jython 或 IronPython 等无 GIL 解释器的应对破局方案。

## Atmosphere & style 整期视频呈现出严谨扎实却又通俗易懂的硬核技术分享风格,高天的讲解条理清晰、抽丝剥茧。他善于运用“上厕所锁门排队”等生动的日常比喻,将晦涩的底层锁机制与复杂的操作系统原理讲得趣味盎然。观看体验既像是跟随经验丰富的前辈拆解 CPython 底层源码的幕后设计,又充满客观理性的工程权衡思考,让观众在理解技术历史局限性的同时收获开阔的技术视野。

## Bulletin - [02:30](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=150s) GIL 的诞生本质上是为了解决 CPython 内存管理中“引用计数”在多线程环境下的非原子性并发竞争问题。 - [07:15](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=435s) GIL 方案长期得以保留的核心原因在于其架构实现简单、天然避免了死锁,并且最大程度保障了单线程性能与 C 扩展生态的开发便利性。 - [09:30](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=570s) 诞生于单核时代的 GIL 在如今多核 CPU 普及的背景下成为瓶颈,导致 Python 无法利用原生多线程有效加速 CPU 密集型任务。 - [10:45](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=645s) 目前开发者规避 GIL 限制的主要方案包括使用 multiprocessing 模块进行多进程并发、在底层 C 扩展中实现多线程,或选用 Jython 等无 GIL 替代解释器。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=0s) 同一进程内的多个线程共享全部内存资源,且线程间的调度顺序不可控,这导致多线程在并发读写共享变量时极易触发竞争冒险(Race Condition)。 - [02:30](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=150s) CPython 依赖引用计数进行自动内存管理,当计数归零时立即回收内存。 - [03:05](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=185s) 引用计数的底层修改在 CPU 指令集层面非原子操作,多线程并发交错修改会导致计数错误,从而引发内存泄漏或悬空指针崩溃。 - [05:00](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=300s) 为每一个底层 PyObject 对象单独加细粒度锁会导致设计复杂度爆炸,CPython 最终选择在解释器核心引入唯一的全局解释器锁(GIL)。 - [06:15](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=375s) 在 CPython 的核心执行循环中,线程在运行字节码之前必须先持有 GIL,从而确保单条字节码运行期间底层的 C 语言操作天然具备线程安全。 - [07:15](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=435s) GIL 的架构极简性大幅降低了代码维护成本,并且单全局锁的设计从根本上消除了多锁交叉引起的死锁隐患。 - [08:00](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=480s) 细粒度锁会在单线程中产生频繁的加锁与解锁开销,而 GIL 确保单字节码最多加锁一次,保障了单线程运行性能。 - [08:30](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=510s) GIL 为早期 C 扩展开发者免去了繁重的线程同步负担,是促成 Python 生态繁荣的重要基石。 - [08:55](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=535s) 历史上移除 GIL 的尝试屡屡受挫,核心障碍在于无法避免单线程性能倒退,且会严重破坏现有海量第三方 C 扩展的向后兼容性。 - [09:30](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=570s) Python 诞生于 20 世纪 90 年代初期,单核时代的全局锁能够很好满足“时间片切片”并发需求,但与现代多核 CPU 的并行架构产生了严重冲突。 - [10:10](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=610s) 由于单解释器进程在任意时刻仅允许单个线程执行字节码,Python 原生多线程在 CPU 密集型任务中无法利用多核提升性能。 - [11:10](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=670s) 使用 multiprocessing 模块开启多进程是规避 GIL 束缚最常见且最 Pythonic 的方案,因为每个进程拥有独立的解释器和内存空间。 - [11:40](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=700s) 将计算密集型逻辑与多线程交给底层 C 扩展执行可突破 GIL,但开发者必须自行承担底层线程安全的维护工作。 - [12:05](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=725s) Jython 和 IronPython 等替代解释器没有 GIL,而 PyPy 解释器内部依然实现了 GIL。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=0s) 线程、进程与竞争冒险(Race Condition)

在探讨 GIL(Global Interpreter Lock,全局解释器锁)之前,首先需要理清操作系统层面的基础概念。程序都是运行在线程中的,线程是操作系统进行计算和调度的最小单位,每个线程拥有属于自己的上下文;而进程则是比线程更大的单位,拥有独立的内存等资源。一个进程内部可以包含多个线程,这些线程会共享该进程拥有的全部内存空间。换句话说,同一个进程内的多个线程都可以直接读写相同的变量。

当一个进程包含不止一个线程时,就会引出一个经典的并发问题——“竞争冒险”(Race Condition)。一个进程内的若干线程既可能在多核上同时运行,也可能在单核上交替调度运行,但无论运行方式如何,程序员都无法预先控制它们之间的相对执行顺序。码农高天举了一个直观的代码示例:假设线程 1 与线程 2 共享同一个变量 `A`,且两者都成功将 `A` 初始化为 `1`。线程 1 首先执行条件判断 `if A > 0:`,判断结果为真,正准备进入条件块执行 `A -= 1`;然而在线程 1 真正执行减法之前,操作系统调度切换到了线程 2,线程 2 同样去判断 `if A > 0:`,此时 `A` 依然是 `1`,因此线程 2 也顺利进入了条件块。最终结果是,两个线程都执行了减法操作,导致 `A` 被减了两次,而该程序本来的业务逻辑显然是希望将 `A` 减到 `0` 为止。码农高天总结道:“像这种由于线程之间的相对运行顺序不同导致的结果不同的情况,我们就叫做 racing condition,或者叫做竞争冒险。”

```mermaid flowchart TD accTitle: 竞争冒险执行时序 accDescr: 展示两个并发线程如何因为缺乏同步而错误地对共享变量执行两次减一操作。

subgraph Thread1 ["线程 1"] T1_Check["判断 if A > 0 (A 为 1, 成立)"] T1_Sub["执行 A -= 1 (A 变为 0)"] end

subgraph Thread2 ["线程 2"] T2_Check["判断 if A > 0 (A 仍为 1, 成立)"] T2_Sub["执行 A -= 1 (A 变为 -1, 产生 Bug)"] end

T1_Check --> T2_Check T2_Check --> T1_Sub T1_Sub --> T2_Sub ```

---

[02:30](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=150s) 内存管理与引用计数的并发危机

在 C 与 C++ 等传统底层语言中,程序员必须显式地分配与释放内存;如果只分配而不释放,内存占用就会持续累积直至耗尽。而在 Python 中,开发者完全不需要手动管理内存,所有的 Python 对象(如列表、字典等)都是拿来即用。这种便利源于 Python 解释器内部实现的内存管理机制。

在内存管理中,“分配”相对直接,核心难点在于确定“何时可以安全地释放内存”。CPython 采用的基石机制是“引用计数”(Reference Counting)。其原理十分清晰:每一个 Python 对象内部都会记录当前有多少个地方引用了自己。每当出现一个新的引用,对象的引用计数就加 1;当引用失效或对象离开作用域时,引用计数就减 1。一旦引用计数归零,解释器就知道没有任何地方再需要该对象,便可以直接调用底层释放函数(如 `Py_DECREF` 中的内存回收逻辑)将其占用的内存释放掉。

然而,引用计数一旦置于多线程环境下,就会立刻遭遇致命的竞争冒险: - **操作非原子性**:虽然在 C 语言源码中对计数的减少看起来只有一行(例如对 `ob_refcnt` 进行 `--` 操作),但它并不是原子操作(Atomic)。在底层 CPU 指令集层面,该操作必须拆分为三个离散步骤:读取计数值、在寄存器中减 1、将新值写回内存。 - **并发交错的灾难**:在这三个微步骤执行的间隙,其他线程极有可能插入并同时修改该计数。如果发生交错,就会导致计数出现多数或少数的偏差。 - **后果**:一旦引用计数紊乱,Python 解释器就无法准确判断对象的生命周期,进而导致极为严重的内存泄漏,或者过早释放内存引发悬空指针崩溃。

---

[05:00](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=300s) 加锁机制与 GIL 的诞生

在多线程编程中,解决竞争冒险最通用的方案是“加锁”(Locking)。加锁的本质是确保受保护的代码段在同一时刻仅能被一个线程执行。码农高天打了一个非常生动的比方:“你可以理解为你上厕所的时候有一个隔间,你进去之后你先把门插上,然后你在里面干什么事别管,剩下的人想进这个门要先排队,等你出来之后他才能进去。”

面对引用计数的并发修改,最直观的设想或许是在每次修改前显式加锁。但 Python 面临的并发问题远不止引用计数这一个点——几乎所有操作 Python 内部对象(`PyObject`)的底层 C 代码,都存在被多个线程同时读写的风险。如果为每个对象的细粒度访问分别加锁,系统的设计与维护复杂度将呈指数级爆炸。因此,Python 的核心设计者们采取了一种更为彻底的手段:为整个解释器引入一把全局锁,即 Global Interpreter Lock(GIL)。

在 CPython 解释器的核心主循环文件 `ceval.c`(约 1430 行的 `mainloop`)中,线程在运行前必须调用特定函数去获取这把全局锁。其调度逻辑保证了:如果线程收到了释放请求,会先释放锁再尝试重新获取。通过这种机制,Python 确保了任何一个线程在执行字节码(bytecode)时都必须持有该全局锁。换句话说,没有任何一条字节码的执行过程会被其他线程抢占打断。在单条字节码运行期间,底层的 C 语言代码天然处于线程安全状态,增减引用计数等底层操作均无需担心并发冲突。

---

[07:15](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=435s) 天使的一面:GIL 为何至今未被移除?

尽管如今 GIL 常被很多开发者诟病为阻碍 Python 迈向高性能并发的“桎梏”,但它在早期被采用并沿用至今,具备一系列非常关键的技术红利:

1. **架构极度简单且易于维护**:相较于为每个对象单独设计细粒度锁,全局锁的方案实现成本极低。码农高天特别强调了工程落地的核心法则:“越简单的程序你越有可能写对,你在维护上所需要的努力就越小,而这种全局锁相对于你可能每一个 object 都实现一个自己的锁要简单非常多。” 2. **根除死锁隐患**:当一个程序中同时存在两个及以上的锁时,极易因锁的争抢顺序不一致而触发死锁(Deadlock)。由于 GIL 是唯一的全局线程锁,解释器核心逻辑天然避免了多锁交叉死锁的风险。 3. **单线程执行性能极佳**:加锁和解锁本身都伴随着不可忽视的时间开销。GIL 保证了每条字节码执行最多只需要获取一次锁;如果改成每个对象各带一把锁,单条字节码在访问多个对象时就需要频繁加锁解锁数十次,这将大幅拖垮单线程程序的运行性能。 4. **极大地降低了 C 扩展开发门槛**:在字节码级别天然杜绝了竞争冒险后,第三方开发者编写 C 扩展(C Extension)去操作 Python 对象时,根本不需要处理繁杂的线程同步问题。Python 能拥有今日庞大的生态繁荣,很大程度上归功于早期 C 扩展编写的低门槛。

历史上并非没有人尝试彻底拿掉 GIL,但以往所有的尝试都面临无法逾越的阻碍:没有一种方案能在移除 GIL 的同时保证单进程单线程下的性能不发生明显倒退。此外,另一个更严峻的挑战在于向后兼容性(Backward Compatibility)——海量现存的第三方 C 扩展库在编写时均默认持有 GIL,一旦贸然移除,整个既有生态的代码都将面临大面积损坏。

---

[09:30](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=570s) 恶魔的一面与多核时代的破局之道

讨论完 GIL 的优势,就必须直面其被广泛诟病的“恶魔”一面。理解这一缺陷需要结合计算机体系架构的发展历史。很多人潜意识里觉得 Python 是一门新兴语言,但它实际上诞生于 20 世纪 90 年代初期,甚至比 Java 问世还要早。在那个年代,消费级多核处理器基本不存在,多线程技术的核心价值在于让任务轮流执行,防止某一个耗时计算卡死整个界面或阻塞其他任务,本质上是“时间片切片并行”。由于物理上只有一颗 CPU 核心,在当时使用单一的全局解释器锁完全没有暴露缺陷。

然而迈入 21 世纪后,多核心 CPU 已经成为计算机的绝对标配,操作系统的多线程能够被真正分配到各个独立的物理核心上并行运行。例如原本需要计算 100 个数字,在 4 核 CPU 上让每个核心分担 25 个,理想情况下速度可以直接提升 4 倍。但在这种硬件背景下,GIL 产生了严重的“水土不服”:因为单个 Python 解释器进程在任意时刻只允许唯一一个线程执行字节码,无论代码创建了多少个线程,物理上同时运行 Python 代码的永远只有一个核心。这直接导致 Python 的原生多线程无法通过多核并行来加速 CPU 密集型任务。

```mermaid flowchart LR accTitle: 多核硬件下的 GIL 瓶颈 accDescr: 对比操作系统多核 CPU 与 Python 解释器受制于 GIL 的执行瓶颈。

subgraph Hardware ["多核 CPU 硬件"] C1["Core 1"] C2["Core 2"] C3["Core 3"] C4["Core 4"] end

subgraph CPython ["CPython 解释器"] GIL{{"GIL 全局锁"}} T1["线程 1 (持有锁运行中)"] T2["线程 2 (阻塞等待锁)"] T3["线程 3 (阻塞等待锁)"] end

T1 -.->|"占用"| C1 GIL --- T1 GIL -.->|"互斥拦截"| T2 GIL -.->|"互斥拦截"| T3 ```

面对这一硬件时代的代沟,Python 社区演化出了多种规避与破局方案: - **多进程(`multiprocessing`)**:这是最简单、也最具“Pythonic”风格的解决方案。由于 GIL 仅存在于单个解释器进程之内,通过开启多个独立的进程,每个进程拥有独立的解释器与内存空间,便能完美绕开 GIL 的束缚,将所有的 CPU 物理核心跑满。 - **编写 C 扩展并在底层实现多线程**:开发者可以在 C 扩展中编写密集计算逻辑,并将多线程派发给底层的 C 代码运行,此时脱离了 Python 字节码解释层,自然可以实现真正的多核多线程并行。但这种方式要求开发者必须自行处理底层线程的竞争冒险问题,尤其在并发访问 Python 原生对象时需要极度小心。 - **选用无 GIL 的替代解释器**:除了官方的 CPython 之外,市面上还存在其他解释器实现。例如运行在 JVM 上的 Jython 以及基于.NET 的 IronPython 内部都没有实现 GIL;不过需要注意的是,以 JIT 编译著称的 PyPy 解释器依然包含 GIL。

## Notable quotes > [07:15](https://www.youtube.com/watch?v=XjBsk8JGHhQ&t=435s) 越简单的程序你越有可能写对,你在维护上所需要的努力就越小,而这种全局锁相对于你可能每一个 object 都实现一个自己的锁要简单非常多。 — 码农高天

## Mentioned 码农高天, Python, CPython, GIL, C, C++, Java, JVM, Jython, .NET, IronPython, PyPy

## Video description 这期视频讲一个呼声很高的话题,GIL。很多人对GIL一知半解,甚至有人不知道它是个什么东西。这期我们从它的由来现状优缺点等等各种方向仔细聊聊,一期视频,你就再也不迷茫了!

[Source](https://www.youtube.com/watch?v=XjBsk8JGHhQ)

中文简报

CPython靠GIL维系单线程性能与生态 锁死多核原生算力加速 GIL通过全局互斥执行消除了引用计数的并发冲突并降低了C扩展门槛,但也导致解释器无法在多核时代实现原生多线程并行计算,迫使密集任务依赖多进程规避性能瓶颈。 内容从操作系统底层的线程、进程及竞争冒险等基础概念切入,深入剖析了 CPython 采用引用计数进行内存管理时面临的并发危机,进而解释了引入全局解释器锁(GIL)的根本原因。随后,作者从“天使”的一面论证了 GIL 具备架构极简、杜绝死锁、保障单线程高效运行以及大幅降低 C 扩展开发门槛等巨大红利,说明了其长期无法被轻易移除的技术原因。最后,作者直面 GIL 在现代多核硬件下导致原生线程无法实现 CPU 密集型任务并行的“恶魔”一面,并为开发者梳理了多进程(multiprocessing)、底层 C 扩展多线程以及选用 Jython 或 IronPython 等无 GIL 解释器的应对破局方案。 GIL 的诞生本质上是为了解决 CPython 内存管理中“引用计数”在多线程环境下的非原子性并发竞争问题。 (2:30) GIL 方案长期得以保留的核心原因在于其架构实现简单、天然避免了死锁,并且最大程度保障了单线程性能与 C 扩展生态的开发便利性。 (7:15) 诞生于单核时代的 GIL 在如今多核 CPU 普及的背景下成为瓶颈,导致 Python 无法利用原生多线程有效加速 CPU 密集型任务。 (9:30) 目前开发者规避 GIL 限制的主要方案包括使用 multiprocessing 模块进行多进程并发、在底层 C 扩展中实现多线程,或选用 Jython 等无 GIL 替代解释器。 (10:45)

Reading summary

码农高天在视频中解析了 CPython 全局解释器锁(GIL)的设计根源与权衡,指出 GIL 最初是为了保护引用计数等底层内存管理逻辑免受竞争冒险破坏而设立,虽带来了架构简洁与优良的单线程性能,却也在多核硬件时代限制了原生多线程的 CPU 并行能力。 竞争冒险与引用计数的并发危机 (0:00) 在操作系统中,同一个进程内的多个线程共享全部内存空间,且相对执行顺序无法预先控制。当多个线程同时读写同一个变量(例如检查条件并执行减法)时,极易因执行交错而产生竞争冒险。在 CPython 的自动内存管理体系中,对象生命周期的核心是引用计数:当对象被引用时计数加 1,离开作用域时减 1,计数归零则释放内存。然而,底层对引用计数的修改并非原子操作,需要经历读取、修改、写回等分步 CPU 指令。多线程并发修改会导致计数偏离真实值,进而引发严重的内存泄漏或过早释放导致的程序崩溃。 全局解释器锁的设计与工程红利 (5:00) 为了避免给每个内部对象分别设计细粒度锁所带来的架构复杂度爆炸,CPython 采取了为整个解释器引入单一全局锁的机制。在解释器主循环(`ceval.c`)中,线程执行每条字节码之前都必须获取 GIL。这意味着单条字节码的底层 C 执行过程不会被其他线程抢占打断,底层数据结构天然获得线程安全。码农高天指出,这种全局方案工程实现极度简单、维护成本低,彻底杜绝了多锁交叉导致的死锁风险,并让单线程执行免受频繁加锁解锁带来的性能损耗。同时,它极大降低了第三方开发者编写 C 扩展的门槛,成为 Python 早期生态繁荣的重要基石。 多核硬件下的性能瓶颈与历史阻碍 (9:30) Python 诞生于 20 世纪 90 年代初期,当时的消费级计算机普遍采用单核 CPU,多线程的核心目的是基于时间片切片实现轮流调度,因此 GIL 的缺陷并未显现。然而在当代多核硬件普及后,操作系统能够将多个物理核心分配给不同的线程运行,但 CPython 单个进程在任意时刻仅允许单一线程执行字节码,导致原生多线程无法通过利用多物理核心来加速 CPU 密集型任务。以往试图彻底移除 GIL 的尝试均面临两大阻碍:无法在移除锁的同时保证单进程单线程性能不发生显著倒退,以及海量默认依赖 GIL 线程安全的第三方 C 扩展面临破坏向后兼容性的风险。 现代开发中的应对与破局方案 (9:30) 面对 GIL 对多核计算的制约,开发者演化出了多种规避途径。最通用的方案是使用多进程(`multiprocessing`),通过为每个任务创建拥有独立解释器与内存空间的进程,完全绕过单进程内的 GIL 限制并跑满多物理核心。其次,开发者可以在 C 扩展底层自行管理多线程执行密集计算,脱离 Python 字节码解释层实现并行,但需自行承担底层线程同步的复杂性。此外,市面上还存在天然无 GIL 的替代解释器(如基于 JVM 的 Jython 和基于 .NET 的 IronPython),不过具备 JIT 编译能力的 PyPy 解释器内部依然保留了 GIL。 GIL 是 CPython 权衡简单性、单线程执行效率和生态兼容性的历史产物。虽然它在多核 CPU 时代成为原生多线程并行计算的阻碍,但通过多进程或底层 C 扩展等工程方案,开发者依然能在现有技术限制下充分调用现代硬件的算力。

Source quotes

[7:15] 码农高天: 越简单的程序你越有可能写对,你在维护上所需要的努力就越小,而这种全局锁相对于你可能每一个 object 都实现一个自己的锁要简单非常多。

Original transcript

[0:00] 这期视频我们来讲gil 也就是下面的Global Interpreter Lock讲GIL之前我们首先要理清一个概念什么是线程线程是操作系统进行计算和调度的一个最小的单位我们可以简单的理解为我们的程序都是运行在县城里的每一个县城又属于自己的上下文进程是比现成更大一点的单位每一个进程有自己的内存之类的一个进程可以有好几个线程这些县城会共享进城有的内存就是这些线程都可以读写同样的变量你可以这么理解那有关进程跟现成的这是一个非常大的话题啊大家如果感兴趣的话自己去研究一下这不是我们今天的重点我们只是需要它作为一个背景资料那当一个进程有不止一个县程的时候呢就会出现一种情况叫做raising或者叫竞争冒险因为一个进程中的若干的线程 他们既有可能同时运行 也有可能交替运行 但不管是同时运行还是交替运行 你都没有办法控制他们之间的相对顺序我们举个例子假设两个线程都在运行左边这个函数我们说线乘1跟线乘2假设他们两个都成功的把A处置化成1了注意啊 他们两个是共享A变量的 这是现成的特点那假设限乘1先来判断这个if a大于0 他发现这个是true 对不对 然后他就进入了这个if statement 这个时候 限乘2开始判断了 在限乘1有能力运行a减等于1之前 限乘2也判断if a大于0线乘2也发现A是1大于0没错线乘2就也进这个if block了由于他们两个线乘都进了这个if block所以A被减了两次而左边这个程序显然它的目的应该是把A减到0为止那在更多的情况下可能是线乘1运行然后if a大于0 a减等于1然后a就变成0了这个时候线乘2再来判断if a大于0的时候他就发现a不大于0了然后他又跳过这个if block了那像这种由于线程之间的相对运行顺序不同导致的结果不同的情况 我们就叫做racing condition 或者叫做竞争冒险好 那接下来我们说回python如果你学过C跟C++的话 你会知道在C跟C++里面你是需要显示的去分配和释放内存的如果你光分配不释放随着你程序的运行你占用的内存就会越来越多你的内存就会爆炸对吧但是在python里面你并不需要显示的去分配跟释放内存所有的python object包括list和diction这种东西你拿来直接用就可以了那你不用做这些烦心事 是因为python的解释器的memory management帮你做了这件事Python是怎么做到自动分配 尤其是释放内存的呢为什么要说油漆呢因为分配比较容易对吧我需要那头我拿就行了关键是什么时候可以释放它那python使用的机制叫做reference count引用技术它的原理也并不难理解就是每一个python object他都数着有多少个地方用到了自己没有一个新的地方用到了自己他就把自己的引用技术加一然后这个地方不用了或者说这个object没有了我的引用技术就减一这样只要你数数数的是对的我就可以知道什么时候我的引用技术到零了那就没有人需要我了我自己就可以把我这块内存释放掉了大家可以看这里这个obrefcount就是这个pyobject的引用技术保存的地方这里这个pidecreaseref函数也是比较好读啊就是把这个refcamp-1然后如果它到0了的话就把它deallocate掉这个事情本身不难理解对吧就是数数嘛但是结合我们刚才说的竞争冒险的事情我们可以想一下如果一个进程里有多个线程在运行的话这里就会有一个竞争冒险的问题因为这个简简Obref count并不是Atomic的而他们的意思就是说我在运行的时候不会被其他的线程打断这个- 虽然在c里面看起来像是一个操作图但实际上也要把这个refconf的信息读出来-1再存回去那在这三个步骤中间就有可能有其他的线程过来在你存回去之前也做这件事那这种情况的发生就有可能让你数数数错多数了一个少数了一个这都是有可能的那一旦你数数明白了 是不是你就没有办法保证你的每一个python object都可以正确的被释放了那你就会出现非常严重的泥层泄漏的问题那在县城中一般来说是怎么解决这个问题的呢最常用的方法叫做夹锁加锁是什么意思呢就是我要保证这一段程序只有我一个线程在运行 [5:00] 其他的县城不可以进入这段程序你可以理解为你上课所的时候有一个隔间你进去之后你先把门插上然后你在里面干什么事别管剩下的人想进这个门要先排队等你出来之后他才能进去通过锁这个机制呢他就可以解决这种竞争冒险的问题那我左边给出的是一个尾代码啊 我没有给出来一个能运行的这个multi threading的程序 大家理解一下就行就是在if之前我先锁住他这样我在运行if a大于0 a减等1的时候其他的线程是不能进入到这一个代码之内的他们需要等我release了这个lock之后他才能再运行这一段程序那让我回到python 这个时候你肯定说这个简单对不对 我只需要在这个if外面加一个锁就可以了但是啊python不光是reference count有这个问题它所有跟python object有关的代码都有可能会存在这个问题都有可能有若干的线程同时尝试去读或者写这个python object数据所以说当时设计Python的这些设计者们决定给Python设计一个全局的锁也就是我们所谓的GIL1430行这行代码是在我们之前提到过的ceval.c的mainloop里面它的作用就是拿到这个Global索我们可以进去看一下这个函数里面呢就是这样啊如果他收到了drop的信息的话他会先drop然后take这个函数可以保证在它运行完之后当前的这个县程会拿到GIO锁通过这种机制Python可以保证每一个bytecode在运行的时候都是拿到县城所的换言之 没有bytecode可以被其他的线程所打断这样的话你在每一个by code里面运行的C程序就都是现成安全的你可以在里面大胆的去增加reference count 减少reference count 都不用管锁的事儿 因为你已经知道锁被拿住了那这种全局所有什么好处呢好处还是非常多的你们可能今天会看到很多人诟病说这个python的gill是阻止python前进的这么一个桎梏但是他最开始这么设计绝对是有他的初衷的他的好处非常多第一这是一个非常简单的设计你如果真正写比较大的项目就知道简单真的是一个很重要的事越简单的程序你越有可能写对你在维护上所需要的努力就越小而这种全局锁相对于你可能每一个object都实现一个自己的锁要简单非常多第二由于只有一个县城锁它避免了死锁的问题死锁是什么呢 我们在这里也不过多的介绍啊 但是你要知道当你一个现场里面有两个以上的锁的时候 它就有可能会造成死锁第三对于单线程的程序或者是没法并行的多线程程序这种全局所的性能是非常优秀的大家可以想到这个要所这件事显然是需要一些时间成本对吧那全局锁就保证了你在每一次运行一个bytecode的时候至多只需要要一次锁但是如果你是那种比如每一个object都有自己的锁的话你一个bytecode由于你要access很多个object你就可能要拿很多次锁最后呢就是它让你给这个python代码写c extension变得容易了很多因为你可以确定在每一个bytecode运行的时候没有现成的竞争冒险问题这样你在你的c代码里面去修改python object的时候你就不用管那些乱七八糟的锁了让第三方开发者的编程变得容易了很多其实有人说Python能有今天的成绩跟它的C extension开发比较容易也是有密切的关系的那以上种种的优势就是至今Gill仍然存在在python里的原因古往今来也不是没有人尝试从python里面拿走gill但是没有一次尝试是可以保证python在单进程单线程下的运行速度不受到影响的当然了还有一个非常严重的问题就是所谓的backward compatibility也就是所谓向后兼容就是我之前写的C extension都默认我现在是有线程所的你先把这个东西拿掉了那我那个东西很有可能就不好使好 那我们说了这个gill天使的一面那他被这么多人诟病他自然有恶魔的一面对吧我们就来说说Gil给python带来的很严重的一个问题在说这个问题之前我们首先要回顾一下Python的历史很多人会觉得Python是一个比较新兴的语言 其实Python比Java还要大它是上个世纪90年代初期被开发出来的在那个年代呢多核这种东西几乎不存在多线程存在的意义就是这些线程可以轮流执行不会因为某 [10:00] 变成计算量过大而卡住了其他的事情它就是一个切片并行的理念所以在那个年代呢它本来就只有一块CPU本来就只能运行一个现成的代码所以这个global的interpret lock没什么影响但是进入了21世纪之后 多核现在已经是电脑的标配了就每一个电脑都有很多个CPU核心然后他们可以同时进行计算也就是说一个进程的若干线程可以同时在若干的CPU核上一起跑通过这种并行来增加你程序运行的速度我本来要算100个数对吧但是有4个cpu我让每个CPU算25个数这不速度乘了4吗?在这种情况下,Gill就出现了严重的水土不服因为他每一个Interpreter只允许一个线程运行他的Bitecode所以说你不管有多少个县程只能有一个县程在实际的运行python代码这就导致了python的多线程没有办法利用多核来增加自己的运算速度那这个问题呢在python刚被发明的时候还不是一个问题但是到现在来看了它变成一个大问题这就是时代的改变对语言造成的影响那Python在多核CPU时代就一无是处了吗显然不是 拍档依然是生机勃勃的为什么呢?因为python有其他的方式来避开这个计算的问题那相对来说最简单也最拍档的呢就是用multiprocessing也就是用多进程来解决这个问题就虽然你一个进程没有办法利用多个CPU但是我可以有好多个进程使用多进程就可以避开gill这个问题然后利用多核的cpu给自己的程序加速第二种方式是你可以通过自己写C extension 然后在C里面去做多线程就是你的多个线程是运行的c代码 而不是python代码当然了你就要自己去解决那边的竞争冒险问题包括如果你多个C线程同时尝试access Python 的 object 的话你就要更小心一点当然了 你也可以尝试用一些没有gill的python解释器这个事我们在之前的某期视频也提到过像Jathon 包括Iron Python好像都是没有Deal的但是拍拍是有的好 那这期视频简单的跟大家介绍了一下geel啊 希望对大家有所帮助如果你喜欢的话 别忘了点一下关注 记得一键三楼哦优优独播剧场——YoYo Television Series Exclusive