CPython字节码调度重构收窄GIL释放点 多线程仍须加锁

【python】听说因为有GIL,多线程连锁都不需要了? — 码农高天

码农高天

Python 3.10起仅在跳转与函数调用处检查eval_breaker,虽使简单循环暂时避开竞争冒险,但该行为属内部实现细节而非语言特性,生产环境放弃线程锁将带来严重风险。

AI-assisted summary of 【python】听说因为有GIL,多线程连锁都不需要了? — original source. Check the original for full context.

Source published:

Key ideas

  • 在 Python 3.11 中运行多线程全局累加代码可能不再出现数据丢失,但这并非因为 GIL 让线程变得天生安全,而是底层字节码调度机制变化导致的副作用。 (0:00)
  • Python 3.10 对主循环字节码分发机制进行了重构,常规指令执行完后直接跳转至 predispatch,跳过了顶部释放 GIL 的 eval_breaker 检查。 (5:00)
  • 从 Python 3.10 开始,GIL 的释放基本被严格限制在函数调用(Call)和分支跳转(Jump)指令中发生,顺序执行的简单字节码序列不会在中间被切走。 (5:00)
  • 官方从未将特定字节码序列的原子性作为语言特性(Feature)公开承诺,这种底层行为属于实现细节(Implementation Detail),多线程代码中仍必须规范加锁。 (5:00)

Detailed notes

## Background 该视频由“码农高天”于2024年8月31日发布在YouTube平台上。视频简介指出,本期内容主要探讨GIL(全局解释器锁)的释放机制,并深入剖析为什么在Python 3.10之后经典的竞争冒险示例代码不会报错了。

## TL;DR 视频通过一个经典的多线程计数器累加案例切入,解释了为何在Python 3.11中看似没有发生竞争冒险、结果恰好是200万的现象。作者深入对比了CPython 3.9与3.10+的源码架构,指出早期版本绝大多数字节码在DISPATCH时都会检查eval_breaker并可能释放GIL,而3.10+重构后跳过了常规检查,仅在函数调用和跳转(如循环边界)时才显式释放GIL。作者最后强调,这种现象纯属底层实现细节(Implementation Detail)而非官方语言特性,开发者绝不能以此为由在多线程编程中盲目省略线程锁。

## Atmosphere & style 整体氛围极具硬核技术极客的探索感,节奏紧凑且逻辑缜密。高天的讲解风格清晰直白、循序渐进,既有深入CPython底层C源码与宏定义的硬核剖析,又带着幽默风趣的反思视角。当戳破“有GIL就不需要锁”的误区时,其语调坚定而理性,在展现底层机制趣味性的同时,传递出成熟工程师对代码可维护性与生产规范的敬畏之心。

## Bulletin - [00:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=0s) 在 Python 3.11 中运行多线程全局累加代码可能不再出现数据丢失,但这并非因为 GIL 让线程变得天生安全,而是底层字节码调度机制变化导致的副作用。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) Python 3.10 对主循环字节码分发机制进行了重构,常规指令执行完后直接跳转至 predispatch,跳过了顶部释放 GIL 的 eval_breaker 检查。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) 从 Python 3.10 开始,GIL 的释放基本被严格限制在函数调用(Call)和分支跳转(Jump)指令中发生,顺序执行的简单字节码序列不会在中间被切走。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) 官方从未将特定字节码序列的原子性作为语言特性(Feature)公开承诺,这种底层行为属于实现细节(Implementation Detail),多线程代码中仍必须规范加锁。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=0s) GIL 的核心职责是防止两个线程同时运行字节码,但它无法杜绝多线程间的竞争冒险(Race Condition)。 - [00:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=0s) 在 Python 3.11 下测试两个线程各累加 100 万次的经典代码时,运行结果罕见地达到了完美的 200 万,容易引发“不用加锁”的误解。 - [00:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=0s) 如果将累加和赋值拆分到不同循环或跨越执行边界,或者退回 Python 3.9 环境,竞争冒险会立刻重现。 - [02:30](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=150s) Python 虚拟机的字节码调度在 PyEval_EvalFrameDefault 的 main loop 中运行,通过 eval_breaker 标志位判断当前线程是否需要交出 GIL。 - [02:30](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=150s) Python 3.9 中绝大多数普通字节码结尾采用 DISPATCH 宏(本质是 continue),会直接回到主循环顶部触发 eval_breaker 检查并释放 GIL。 - [02:30](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=150s) 在 Python 3.9 中,counter += 1 在执行完 BINARY_ADD 且写回变量前就可能释放 GIL,被其他抢占执行的线程覆盖结果,导致竞争冒险。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) Python 3.10+ 将 DISPATCH 改为直接 goto 到 predispatch 标签,使常规字节码执行完毕后完全跳过循环顶部的 eval_breaker 检查。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) Python 3.10+ 将 GIL 释放检查收敛到显式调用的 CHECK_EVAL_BREAKER 宏,通常仅在函数调用和分支跳转(Jump)两类指令中触发。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) Python 3.11 中单次循环内的简单累加属于连续顺序指令,中途绝不包含分支或函数调用,因此在计算完成并写回前不会发生线程切换。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) Faster CPython 团队在 3.10 至 3.13 期间对帧循环进行了大规模重构,调度机制属于随时可能变动的底层实现细节而非稳定的语言规范。 - [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) 优秀代码的核心是清晰易懂,在生产环境利用不公开的底层实现细节去“炫技”不加锁,升级版本时会引发严重的稳定性隐患。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=0s) GIL与多线程竞争冒险:Python 3.11 为何“不报错”了?

关于 Python 的全局解释器锁(GIL, Global Interpreter Lock),网上的科普大多只停留在“GIL 是为了防止两个线程同时运行字节码”的层面上,但很少有人会深入探讨其具体的释放机制,即:每一个线程究竟在什么时候才会释放这个锁?

为了说明这个问题,码农高天给出了一个极简的经典竞争冒险(Race Condition)示例代码:定义一个函数 `f`,其逻辑非常简单,就是通过循环让一个全局变量 `counter` 不断自增(`counter += 1`)。接着,程序创建两个线程同时运行这个函数 `f`,各自累加 100 万次。理论上,最终打印出的 `counter` 只有两种可能:要么完美等于 200 万,要么因为多线程竞争冒险导致数据丢失而小于 200 万。

令人意外的是,如果在 Python 3.11 中直接运行这段代码,打印出来的结果居然恰好是 200 万整,竞争冒险并没有出现。这时可能有人会误以为:“这是因为 Python 有 GIL,由于 GIL 的存在,Python 的多线程程序压根就不会出现竞争冒险,也就是说 Python 的线程不用加锁。”面对这种推论,码农高天直言:“绝对不存在什么由于有了 GIL 我的多线程就不需要锁这种说法。”

只要稍微调整一下代码,就能戳破这种错觉: 1. 如果在同一个循环内把 `counter + 1` 先赋值给一个局部变量,再把局部变量赋值回全局变量 `counter`,运行结果依然是 200 万。 2. 但如果将逻辑拆分,比如调换相关执行顺序,让计算和赋值跨越了循环的边界,竞争冒险便会立刻重现,最终结果又不再是 200 万了。 3. 此外,如果退回到 Python 3.9 版本中去跑最原始的累加代码,竞争冒险同样会赫然出现,结果绝不会是 200 万。

要想搞清楚为什么 3.11 和 3.9 表现完全不同,就必须深入 CPython 的源码,去探寻 GIL 究竟是在哪一个具体的执行节点被释放的。

```mermaid flowchart LR accTitle: GIL 释放在 Python 3.9 与 3.10+ 中的行为对比 accDescr: 展示 Python 3.9 依靠字节码 dispatch 频繁检查 eval_breaker,而 Python 3.10+ 改为仅在分支和函数调用时显式检查的过程。

subgraph Py39["Python 3.9 循环机制"] A1["运行字节码 (如 BINARY_ADD)"] --> B1{"宏调度: DISPATCH"} B1 -->|"触发 main loop 顶部"| C1["检查 eval_breaker"] C1 -->|"释放 GIL"| D1["线程切换 -> 触发竞争冒险"] end

subgraph Py310["Python 3.10+ 循环机制"] A2["普通计算字节码"] --> B2{"宏调度: DISPATCH"} B2 -->|"跳转到 predispatch"| C2["跳过 eval_breaker 检查"] E2["分支/跳转/调用 (Jump / Call)"] --> D2["显式 CHECK_EVAL_BREAKER"] D2 -->|"仅在此时"| C1 end ```

---

[02:30](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=150s) Python 3.9 的 GIL 调度:为什么普通累加会触发竞争?

Python 虚拟机的核心执行逻辑位于 `PyEval_EvalFrameDefault` 函数中。在每个 frame(即函数调用栈帧)中,该函数通过一个 `main loop` 负责逐个读取并执行字节码。

在 Python 3.9 的源码设计中,`main loop` 循环的开头会对 `eval_breaker` 进行检查。这个 `eval_breaker` 中存储的关键标志位之一,就是当前线程是否需要交出 GIL。真正执行释放与交接 GIL 的逻辑位于 `_PyEval_EvalFrameDefault` 调用的 `eval_frame_handle_pending` 等函数中。大体上,每一次主循环对应执行一条字节码指令,如果每执行一条指令都去检查一次 GIL,系统开销(overhead)就会过大,因此 Python 做了一定的宏调度优化: - `FAST_DISPATCH`:直接使用 `goto` 跳转到对应的目标标签,从而跳过循环顶部的 `eval_breaker` 检查。 - `DISPATCH`:本质上是一个 `continue`,它会回到 `main loop` 的最上方,直接触发下一次循环并检查 `eval_breaker`。

在 Python 3.9 的代码库中,使用 `DISPATCH` 的字节码占绝大多数(有 100 多个),而使用 `FAST_DISPATCH` 的只有区区 20 多个。这意味着绝大多数普通字节码在执行完毕后,都有可能在下一个循环交出 GIL。

在最原始的 `counter += 1` 语句中,对应的字节码包括 `LOAD_GLOBAL`、`LOAD_CONST` 以及相加计算的 `BINARY_ADD` 等。由于 Python 3.9 中的 `BINARY_ADD` 结尾使用的是 `DISPATCH`,因此在这个加法计算完成后、尚未把新值通过赋值字节码写回 `counter` 之前,当前线程就很有可能触发了检查并把 GIL 交给了另一个线程。另一个线程抢占执行后,疯狂地自增循环了半天;当控制权最终还给原本的线程时,原本的线程才慢悠悠地把很久之前算好的旧结果写回 `counter`,这就导致了另一线程期间所有的计算结果被硬生生覆盖掉,构成了经典的竞争冒险。

---

[05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) Python 3.10+ 的架构重组与“炫技”陷阱

到了 Python 3.10 及以后的版本,底层字节码分发机制发生了非常关键的重构: 1. 原先会触发 `continue` 的 `DISPATCH` 宏被改写成了 `goto _predispatch`,而原有的 `FAST_DISPATCH` 被直接移除。 2. 循环顶部释放 GIL 的逻辑依然存在,但由于普通的 `DISPATCH` 现在全部直接跳向了循环下方的 `predispatch` 标签,绝大多数常规字节码在执行完后根本不会再回到顶部,从而**完全跳过了 GIL 的检查**。 3. 源码中新增了一个名为 `CHECK_EVAL_BREAKER()` 的宏,并且只在极少数与分支、跳转相关的字节码中显式调用。

码农高天强调,在 Python 3.10 之后,显式检查 `eval_breaker` 释放 GIL 的情况基本只收窄到了两个场景: - **函数调用(Function Call)** - **跳转指令(Jump)**:即字节码脱离常规顺序执行,最典型的对应场景就是 `if` 判断、`for` 循环或 `while` 循环等分支控制流。

这也就解释了为何在 Python 3.11 中运行单次循环内的 `counter += 1` 结果会是完美的 200 万:因为整个取值、相加到写回的字节码执行流都是顺序指令,中间根本没有包含任何分支跳转或函数调用,GIL 在这几条指令执行期间是绝对不可能发生转移的。然而,一旦把操作拆成不同的循环,或者引入了包含 jump 的控制逻辑,GIL 依然会在循环边界被释放并触发线程切换,竞争冒险自然就卷土重来。

那么,既然掌握了这个机制,开发者是否可以在工作中写不加锁的多线程代码,甚至在别人质疑时去嘲笑对方“你在第三层,我在第五层”呢?

码农高天给出了斩钉截铁的否定,并给出了两条至关重要的理由: - **拒绝无意义的炫技**:“一个优秀的程序员写出来的代码应该是大家能读得懂的,不是所有人都读不懂的。” - **这是实现细节(Implementation Detail)而非语言特性(Feature)**:“Python 从来没有官方宣布过这个 feature,换句话说现在这个情况它是一个 behavior,它不是一个 feature。”

从 Python 3.10 到 3.13,由 Faster CPython 团队主导的解释器帧循环进行了翻天覆地的重写与激进优化,其底层调度机制随时都可能被再次推翻。如果业务代码依赖这种未公开的底层实现细节,未来必然会在版本升级中遭遇灾难性的后果。码农高天在结尾总结道,这项冷知识“很酷,但是可能对你没什么用”,绝不能成为在实际生产中抛弃线程锁的借口。

## Notable quotes > [00:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=0s) 绝对不存在什么由于有了 GIL 我的多线程就不需要锁这种说法。 — 码农高天

> [05:00](https://www.youtube.com/watch?v=qQt7G5qhRS8&t=300s) 一个优秀的程序员写出来的代码应该是大家能读得懂的,不是所有人都读不懂的。 — 码农高天

## Mentioned 码农高天, Python, CPython, Faster CPython, GIL, Python 3.9, Python 3.10, Python 3.11, Python 3.13

## Video description 这期视频我们来聊一下GIL的释放,为什么在Python3.10之后,经典的竞争冒险代码不出问题了呢?

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

中文简报

CPython字节码调度重构收窄GIL释放点 多线程仍须加锁 Python 3.10起仅在跳转与函数调用处检查eval_breaker,虽使简单循环暂时避开竞争冒险,但该行为属内部实现细节而非语言特性,生产环境放弃线程锁将带来严重风险。 视频通过一个经典的多线程计数器累加案例切入,解释了为何在Python 3.11中看似没有发生竞争冒险、结果恰好是200万的现象。作者深入对比了CPython 3.9与3.10+的源码架构,指出早期版本绝大多数字节码在DISPATCH时都会检查eval_breaker并可能释放GIL,而3.10+重构后跳过了常规检查,仅在函数调用和跳转(如循环边界)时才显式释放GIL。作者最后强调,这种现象纯属底层实现细节(Implementation Detail)而非官方语言特性,开发者绝不能以此为由在多线程编程中盲目省略线程锁。 在 Python 3.11 中运行多线程全局累加代码可能不再出现数据丢失,但这并非因为 GIL 让线程变得天生安全,而是底层字节码调度机制变化导致的副作用。 (0:00) Python 3.10 对主循环字节码分发机制进行了重构,常规指令执行完后直接跳转至 predispatch,跳过了顶部释放 GIL 的 eval_breaker 检查。 (5:00) 从 Python 3.10 开始,GIL 的释放基本被严格限制在函数调用(Call)和分支跳转(Jump)指令中发生,顺序执行的简单字节码序列不会在中间被切走。 (5:00) 官方从未将特定字节码序列的原子性作为语言特性(Feature)公开承诺,这种底层行为属于实现细节(Implementation Detail),多线程代码中仍必须规范加锁。 (5:00)

Reading summary

码农高天指出,“因为有 GIL 所以 Python 多线程不需要锁”是完全错误的推论;Python 3.11 中部分累加代码未出现竞争冒险,纯粹源于底层字节码调度机制的重构,而非官方保证的语言特性。 表面现象与认知误区 (0:00) 在经典的多线程竞争冒险示例中,两个线程各执行 100 万次 `counter += 1` 累加操作。该程序在 Python 3.9 下运行会出现数据丢失,最终结果小于 200 万;但在 Python 3.11 中直接运行,结果却精准等于 200 万整。这种现象容易让开发者误以为 GIL 彻底消除了数据竞争,从而产生多线程编程无需加锁的错觉。然而,一旦将计算和赋值逻辑拆分跨越循环边界,或者退回 Python 3.9 运行,竞争冒险立刻重现,证明 GIL 并不能自动保证业务层面的线程安全。 Python 3.9 的 GIL 释放机制 (2:30) 在 CPython 虚拟机的 `PyEval_EvalFrameDefault` 函数中,主循环(main loop)负责逐条读取并执行字节码,且在循环开头检查存储释放 GIL 标志位的 `eval_breaker`。Python 3.9 内部存在两种宏调度机制:直接用 `goto` 跳过检查的 `FAST_DISPATCH`(仅 20 多个字节码使用),以及本质为 `continue`、会回到循环顶部检查 `eval_breaker` 的 `DISPATCH`(100 多个字节码使用)。 在执行 `counter += 1` 时,加法指令 `BINARY_ADD` 的结尾使用的是 `DISPATCH`。这意味着在加法计算完成但尚未写回 `counter` 之前,当前线程便极易触发 `eval_breaker` 检查并交出 GIL。另一线程插队修改变量后,原线程再将旧计算值写回,导致期间的自增结果被覆盖,从而引发了经典的竞争冒险。 Python 3.10+ 的架构重构与调度收窄 (5:00) 自 Python 3.10 起,CPython 对字节码分发架构进行了大幅优化,原有的 `FAST_DISPATCH` 被移除,而 `DISPATCH` 被改写为 `goto _predispatch`。这一改动让绝大多数常规字节码在执行完毕后直接跳向循环下方的 `predispatch` 标签,完全绕开了主循环顶部的 GIL 检查。与此同时,源码新增了 `CHECK_EVAL_BREAKER()` 宏,并将其显式调用严格限制在两类场景:一是函数调用(Function Call),二是脱离常规顺序执行的跳转指令(Jump,如 `if`、`for`、`while` 等控制流)。 这解释了为何 Python 3.11 中单次循环内的顺序累加能输出 200 万:取值、相加到写回的字节码完全由顺序指令构成,执行期间不可能交出 GIL。但只要引入包含跳转的循环或控制逻辑,GIL 仍会在跳转边界被释放,竞争冒险便会再次暴露。 底层实现细节绝非语言特性 (5:00) 即便理解了 Python 3.10+ 释放 GIL 的具体时机,码农高天也坚决反对利用这一机制编写不加锁的多线程代码。首先,优秀的代码应当具备通用的可读性,而非依赖晦涩机制进行无意义的炫技。更关键的是,这种特定场景下不丢数据的现象仅仅是当前版本的底层行为(Behavior),而非 Python 官方承诺的语言特性(Feature)。Faster CPython 团队在后续版本中持续激进重构解释器循环,若业务逻辑依赖未经保证的实现细节,后续版本升级将面临不可预测的风险。 该机制属于有趣的底层冷知识,但在工程实践中毫无实用价值;多线程操作共享状态时,开发者仍必须显式加锁,切勿将版本演进带来的底层调度细节当作免除线程同步的借口。

Source quotes

[0:00] 码农高天: 绝对不存在什么由于有了 GIL 我的多线程就不需要锁这种说法。 [5:00] 码农高天: 一个优秀的程序员写出来的代码应该是大家能读得懂的,不是所有人都读不懂的。

Original transcript

[0:00] 今天我们来聊一个跟Gill有关的问题啊那关于GIL呢 也就是Global Interpreter Lock 网上有非常多的介绍他们大多会告诉你,Gill这个东西是防止两个线程同时运行字节码的但是呢,他们很少会聊到比较具体的细节比如说每一个县城到底什么时候会释放这个锁我们举一个非常经典而且非常简单的例子,我们看左边这里有个函数F 功能非常简单就是不断的让这个全局变量counter自家我们建立起来了两个线程 让这两个线程都运行这个函数f那这就是一个经典的极简版的竞争冒险的例子请问最后这个打印出来的counter是多少只有两个可能答案对吧要么是200万 要么不是如果你尝试一下你会发现在python3.11版本中他打印出来的值就是200万整也就是说竞争冒险并没有出现那这个时候可能有人就会觉得啊这是因为python有gill由于GIL的存在,Python的多线程程序压根就不会出现竞争冒险也就是说 拍档的线程不用锁这不血淋淋的事实已经摆在眼前了吗counter数的是对的然而如果你尝试用python3.9去运行下这个程序你会发现这个数就不是200万了经典的竞争冒险就出现了那这又是怎么回事呢?是在python某个版本之后,现成就不用锁了吗?我们来把刚才的程序做一下简单的变换把counter加1保存在一个局部变量里然后再把这个局部变量给保存到全局变量我们可以看到 在这个变换之后 结果是不变的然而如果我们调换一下第九行和第十行的顺序就可以看到竞争冒险又出现了这个结果他就不是200万了也就是说绝对不存在什么由于有了gill我的多线程就不需要锁这种说法那想要理解为什么刚才那个经典的竞争冒险例子没有出问题你就必须要回到CPython的原码去了解这个gill是什么时候被释放的我们之前说过,Python的虚拟机逻辑核心在这个PyEval,EvalFrameDefault谈数里。那我们先来看一下3.9版本它是怎么工作的我们看这里就是这个虚拟机的main loop也就是在每一个frame大家可以理解为函数调用里面这个main loop负责一个一个运行字节码而在循环开始的时候,python对这个Evalbreaker进行了一次检查这个Evil Breaker里保存的信息就是是不是需要你交出Gill真正释放GILS的代码在这个意标frame handle pending里带大家简单看一下啊就是在这个地方进行的gill的交换也就是说每一个这个mainloop的循环都会检查一下是不是该放gill那在大概念上每一次循环对应的是一个字节码的运行当然如果每运行一个字节码都要检查一下gill,overhead会比较大所以在这里呢 Python 也做了一定的优化我们看有的字节码在最后的时候使用的是fast dispatch而有的字节码使用的是dispatch这两个macro的区别呢就是fast dispatch会直接沟吐到这个label上而这个label会跳过Gill的检查而dispatch就是一个continue也就是直接开始下一次循环简单的说,Fast Dispatch不会触发Gear的检查,而Dispatch会触发Gear的检查而我们可以看到在python3.9版本的原代码里面使用dispatch的字节码占大多数一共有100多个而使用fast dispatch只有20多个因此在python3.9版本里面大部分的字节码在运行之后都有可能交出gill我们看我们最开始的例子里在做counter等于counter加1的时候,我们做了一个load global,做了一个load const然后做了一个Binary Ad而python 3.9版本里面,binary add使用的是dispatch也就是说在BinaryAdd之后有可能他会交出Gill也就是说这个计算结果还没有被保存到counter里面它的运行权就交给了另外一个线程另外一个县程呢在那把counter加加加加了半天之后把运行权返回给现在这个县程现在这个县程在把很久之前做的那个计算结果保存到了counter里这就形成了一个经典的竞争冒险也就是另外那个县城刚才加了半天都白加了新的版本有什么不同呢 我们带大家看一下python3.10版本的代码这里我们注意一下一个最重要的变化Dispatch这个 macro变成了GoToPredispatch而之前的那个dispatch fast已经没有了同时增加了一个新的 macro check evil breaker而这个 [5:00] 这个指标breaker 是在检查是不是要释放gill了如果要释放gil了,做一个continue在mainloop这里最上面的检查依然在也就是说如果你做了continue,他依然会去尝试释放gill但是,他在下面增加了这个Predispatch的标签换句话说如果你只是做dispatch这件事情的话你就会跳过Gill的检查而我们可以看到所有的字节码最终使用的都是dispatch也就是说绝大部分字节码的运行根本就不会触发GIL的检查而只有少数的几个跟branch有关的字节码里面才有这个显示的check eval breaker这个check evil breaker基本只出现在两个情况下第一就是函数调用第二就是我们所谓的jump那 jump 简单的理解就是字节码不顺序运行了他非常常见的对应情况有if statement或者for loop或者while loop也就是说在3.10和以后的版本中,只有函数调用和这些branching operation会触发GIL的转移这也就是为什么我们用3.11版本的python跑这个程序发现结果是对的因为在我们做counter等于counter+1的时候,gill是不可能转移的但如果我们把计算和复值分开,在两次循环中分别进行竞争冒险就又重新出现了因为每一次循环都包含 jump而Jump可能会触发Gill的转移好 如果你能坚持看到这里 你已经学会了一个大多数人都不知道的知识现在问题来了你可以使用这个冷知识吗换言之我可以把代码写成左边这样然后在别人质疑我的时候嘲笑他吗这种代码是不是所谓的你在第三层我在第五层呢答案很简单啊不是你依然不应该写像左边这样的代码即使你了解他背后的原理以及确定他在当前的版本中不会出现问题原因有两个第一不要炫技一个优秀的程序员写出来的代码应该是大家能读得懂的,不是所有人都读不懂的第二从某种程度上比第一更重要Python从来没有官方宣布过这个feature换句话说现在这个情况它是一个behavior它不是一个feature这个部分属于implementation detail,它在未来的python版本中随时有可能会变化而事实是从python3.10版本到python3.13版本这个Evil Framed Loop真的是有着翻天覆地的变化的Faster C Python 团队在这几年中对整个这一块进行了大量的改动所以你的代码绝不应该依赖这个feature综上所述,这次我们聊的知识秉承了我们一贯的传统他很酷但是可能对没什么用好的,这期视频就到这里,喜欢的话记得点下关注,别忘了,一键三连哦!优优独播剧场——YoYo Television Series Exclusive