> For the complete documentation index, see [llms.txt](https://blog.wh2099.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://blog.wh2099.com/python/pycon-china-2026.md).

# 让我们自由自在地并发

PyCon China 2026 上海场演讲稿。

## 第一章：限制解放——引言

建议时长：7—8 分钟 章节作用：完成开场、承接前场演讲、建立全场分析框架，并给出后续章节使用的历史坐标

### 一、开场：从《爱》到限制解放

会前播放小虎队的《爱》，登台后从歌名进入主题。

#### 开场口语锚点

> 大家好。 刚才休息时放的是小虎队的《爱》。
>
> 今年 PyCon China 的主题是 AI for Good。 去掉汉语拼音的声调，《爱》也可以写成 AI。 这大概是我最早认识的 AI。
>
> AI 有了，接下来是 `for Good`。 对我来说，就是有了能力，还要把它用好。
>
> 前面几位讲者已经介绍了 GIL、Free-Threading 的迁移和相关的 CPython 实现。 没有听到也没关系。 我这一场从早期历史讲起，系统梳理 Python 的并发演进。
>
> Python 的并发和并行能力一直在增强。 今天我们想问的是：怎样才能并发得好？
>
> 这就是今天的题目：让我们自由自在地并发。

***

### 二、限制器，也是装甲

> 这些年，说起 Python 并发，GIL 往往最先被提到，也往往最先挨骂。 它是 Python 并发限制中最有名的一个。
>
> 但 Python 在并发上的限制远不止 GIL。
>
> 同步调用栈让普通函数沿着调用链运行到返回。 GIL 让同一解释器中的线程不能并行执行 Python。 进程和解释器边界让普通可变对象不能直接共享。
>
> 它们限制的是不同能力。
>
> 《EVA》里，人们先把 EVA 身上的外层看作装甲，后来才知道它主要是限制力量的拘束器。
>
> Python 正好反过来。 我们先看到的是限制，真要拆的时候，才发现它们也在保护 Python。
>
> 以 GIL 为例。 它让线程不能并行执行 Python，也承担了 CPython 内部的一部分同步责任。 它不保证应用程序的并发正确性，但这部分责任不会随着 GIL 一起消失。
>
> 所以我们要看的，是这些限制的两面：它们限制了什么，又保护了什么。 限制解除后，原有的结构和保护需要由其他机制接手。
>
> 我们从三个方面来看。

| 方面   | 代表性限制                  | 问题        |
| ---- | ---------------------- | --------- |
| 任务结构 | 普通函数通常沿调用栈运行到返回        | 任务怎样被组织？  |
| 执行模型 | 同一解释器中的线程不能并行执行 Python | 任务怎样真并行？  |
| 状态模型 | 普通可变对象不能直接跨越进程或解释器边界   | 任务怎样共享状态？ |

每章都从相应的早期历史讲起。

下一章先看任务结构，从 PEP 255 开始：

> 函数可以暂停以后，任务该怎样被组织？

***

## 第二章：任务怎样被组织

建议时长：6—7 分钟\
章节作用：从函数暂停讲到结构化并发，回答任务由谁创建、等待、取消和收尾

### 一、暂停

普通函数沿调用栈运行，直到返回或抛出异常。 调用关系、局部状态和收尾位置都留在栈上。

2001 年，PEP 255 引入生成器。 函数执行到 `yield` 时保存现场，把控制权交还给调用者，下一次再从原地继续。

最初的生成器不允许在 `try` 块里 `yield` 后再由 `finally` 收尾。 暂停一旦脱离调用栈，清理就需要新的规则。

PEP 342 随后增加 `send()`、`throw()` 和 `close()`，补上通信、异常和关闭。 PEP 380 又用 `yield from` 连接内外两层生成器，让委托可以保持原来的控制流语义。

> `yield` 让函数学会暂停，也让暂停之后的责任第一次显现出来。

***

### 二、等待

PEP 3156 把事件循环、Future 和 Task 带进 `asyncio`。 PEP 492 随后引入 `async def` 和 `await`。

`await` 标出一次可能发生的暂停。 当前工作需要等待时，事件循环可以先推进其他任务，结果就绪后再回到原处。

协程对象描述一段可运行的工作。 Task 把它交给事件循环调度，并记录完成、失败和取消状态。

控制流至此可以在一次等待中切换，也可以从一个任务分出更多任务。

***

### 三、归属

Task 带来了一个直接的问题：它可以活得比创建它的函数更久。

```python
async def handle_request():
    asyncio.create_task(write_access_log())
    return response
```

函数返回了，日志任务可能还没结束。 谁来等它？出了错谁处理？需要停下时，谁负责取消和收尾？

结构化并发把这些责任放回作用域：子任务在这里创建，离开前就要等它们结束。

> 控制流可以分叉，责任必须有归属。

***

### 四、责任

Python 3.11 的 `asyncio.TaskGroup` 把子任务收进明确的作用域。

```python
async def load_page():
    async with asyncio.TaskGroup() as group:
        profile = group.create_task(load_profile())
        posts = group.create_task(load_posts())

    return render(profile.result(), posts.result())
```

离开 `async with` 以前，任务组会等待其中的任务。 一个任务失败时，任务组取消其余任务，等待清理完成，再用 `ExceptionGroup` 汇总异常。

调用栈由此扩成责任树。 函数负责局部工作，任务组负责其中创建的子任务。

仍处于 Draft 状态的 PEP 789 又把这条边界推进了一步。 它计划限制取消作用域里的 `yield`，避免父栈帧暂停后把仍在运行的子任务留在外面。

***

### 五、组织

回头看，从调用栈到责任树，延续的是结构化编程的思路。

结构化编程用顺序、分支、循环和函数组织控制流。 结构化并发则用作用域组织任务：子任务在这里创建，离开前要等它们结束，取消、异常和清理也有了明确的归属。

任务组可以层层嵌套。 这样，我们既能看懂局部，也能逐层组合。

不过，组织好任务，还不等于用上多个核心。 并发是多项任务在一段时间里共同推进，并行是同一时刻执行。 接下来就看：任务在哪里执行，才能真正并行？

***

## 第三章：任务怎样真并行

建议时长：5—6 分钟\
章节作用：梳理多进程、多解释器和自由线程三条并行路线

### 一、进程

2008 年，PEP 371 把 `multiprocessing` 带进标准库。

每个进程有自己的地址空间、解释器和 GIL。 操作系统可以把它们放到不同核心上，让 CPU 密集的 Python 代码同时执行。

进程边界也带来了启动、内存和通信成本。 参数与结果通常需要经过进程间通信和序列化。

> 这就是去年提到的"派特"：Python 特色并发道路。 限制没有让 Python 停下，它沿着多进程、协程和多解释器走出了自己的路线。

***

### 二、解释器

子解释器在 CPython 中存在了很多年。 早期的解释器共享不少运行时状态和同一把 GIL，主要用于隔离模块与命名空间。

每个解释器要拥有自己的 GIL，运行时状态也要先移入各自的解释器。 CPython 多年的模块状态与运行时改造，为这一步铺平了道路。

PEP 684 在 Python 3.12 中加入 per-interpreter GIL。 PEP 734 又在 Python 3.14 中带来 `concurrent.interpreters`，并催生了 `InterpreterPoolExecutor`。

多个工作线程由此可以分别进入各自的解释器，在同一个进程里并行执行。 模块、全局变量和大部分 Python 状态留在各自的解释器中。

***

### 三、线程

自由线程选择直接打开同一个解释器里的线程并行。

PEP 703 让自由线程构建在 Python 3.13 进入实验阶段。 PEP 779 又让它在 Python 3.14 进入正式支持、仍然可选的阶段。 默认构建将在后续阶段继续评估。

GIL 退场后，CPython 用更局部的机制保护引用计数、内存管理和内置容器。 应用自己的共享状态，则由应用锁和并发协议负责。

> Free-Threading 把保护从一把全局锁，推向真正需要顺序的位置。

***

### 四、并行

三条路线会长期共存。

| 路线   | 并行位置       | 数据边界    |
| ---- | ---------- | ------- |
| 多进程  | 多个操作系统进程   | 地址空间隔离  |
| 多解释器 | 同一进程的多个解释器 | 解释器状态隔离 |
| 自由线程 | 同一解释器的多个线程 | 直接共享对象  |

多进程提供最强的边界，多解释器在一个进程里隔开 Python 状态，自由线程直接共享同一批对象。 协程继续负责组织等待，可以运行在这些执行路线之上。

任务怎样真并行？

> 让任务进入可以同时执行的不同位置：不同进程、不同解释器，或者自由线程下的不同线程。

任务可以同时执行了，那它们用的数据呢？ 多个任务读写同一份状态时，就可能遇到竞态。

竞态，顾名思义，就是任务之间在竞争状态。 谁先读、谁后写，可能改变最后的结果。

接下来，我们看第三个问题：任务怎样共享状态？

***

## 第四章：任务怎样共享状态

建议时长：8—9 分钟\
章节作用：从状态的访问边界讲到同步保护，回答并行任务怎样安全地交换和共同维护数据

先看一份状态归谁管、谁能改，再决定怎样保护它。

### 一、本地 / 传递 / 不变

最省事的办法，是先减少共同写入。

**本地**，就是把状态交给一个任务或执行域维护。 比如做统计，各个任务先记自己的数，最后再汇总，就少了反复争用同一个计数器。 进程和解释器的边界都能限制其他任务直接访问。

**传递**，就是通过值或消息交接数据。 进程池和解释器池通常会序列化参数，再在接收端重建对象。 队列则把数据从一个任务交给另一个任务。 不过，`queue.Queue` 传的是对象引用，放进去以后，原来的任务仍然可以修改它。 所以还要约定：交接之后，这份数据由谁来改？

**不变**，就是让多方共同读取一份不会变化的状态。 比如一份由字符串、数字组成的固定配置，大家只读，就不用协调对它的修改。 Python 对不可变映射的探索从 PEP 416、PEP 603 延续到了 PEP 814，内置 `frozendict` 将在 Python 3.15 中提供浅层不可变的映射。 但要留意"浅层"：映射里的键值对应关系不能改，值如果是列表，列表里的内容仍然可以变。

***

### 二、同步

不过，有些状态仍要共同修改，这时就需要同步。

#### 竞态

```python
if key not in cache:
    cache[key] = build_value(key)
```

两个线程可能同时通过检查，又分别计算和写入。 即使这里的单次字典访问可以安全完成，整段"检查—计算—写入"的结果仍然取决于时序。

应用需要为这段状态变化规定顺序。 可以用锁包住完整操作，也可以把修改交给一个所有者，再通过队列提交请求。

应用里的缓存需要协调，CPython 自己维护的状态也一样。 过去，GIL 承担了其中一部分同步；它退场以后，这些保护要由其他机制接手。

我们从 PEP 703 的五类改造来看。

#### 引用计数

引用计数本身就是一份高频变化的共享状态。 如果每次增减都修改同一个原子计数器，多核之间就会不断争用同一份数据。

偏置引用计数把计数分成本地和共享两部分，让对象的所属线程走更轻的本地路径。 其他线程访问时，再更新共享计数。

对象永生化让 `None`、布尔值和部分常用对象不再修改引用计数。 延迟引用计数跳过部分栈上增减，堆类型还会把计数分散到各线程，最后由运行时统一汇总。

#### 内存

`pymalloc` 的线程安全依赖 GIL，自由线程构建因此改用 mimalloc 分配 Python 对象。

垃圾回收器可以沿着分配器的内存页寻找对象，不再维护一条由所有 GC 对象组成的全局链表。 原来共享的空闲对象列表也移入 `PyThreadState`，让各线程复用自己的缓存。

mimalloc 还为后面的乐观读取提供了内存管理基础。

#### 垃圾回收

自由线程下，垃圾回收需要先得到一幅稳定的堆视图。 CPython 会让正在执行 Python 代码的线程停下来，再检查循环引用并汇总延迟的引用计数。

这就是停止世界。 为了减少频繁暂停，自由线程构建采用非分代回收，用较少的全堆检查代替频繁的新生代检查。

#### 容器

每个 Python 对象头部都有一把轻量锁。 `list`、`dict` 和 `set` 等容器在修改和大部分读取时，只锁住当前对象。

操作需要同时锁住两个容器时，双对象关键区按地址确定加锁顺序。 嵌套操作需要等待时，Python 关键区会暂挂已经持有的外层锁，之后再恢复。

少数字典和列表操作还会先尝试不加锁的乐观读取。 发现并发修改时，它们退回加锁路径；安全内存回收则推迟相关内存页的复用。

这些机制保护单个容器的内部状态。 回到刚才的缓存，完整的"检查—计算—写入"仍然由应用同步。

#### 执行与扩展

自由线程也改变了解释器和 C 扩展原来依赖 GIL 的约定。

解释器特化时用互斥锁保护内联缓存，并在多线程程序中限制同一条字节码重复特化。 可能被其他线程移除的借用引用，需要换成返回强引用的 C API。

扩展模块还要明确声明自己支持自由线程。 没有声明的扩展会触发警告并重新启用 GIL，自由线程扩展也使用带 `t` 的独立 ABI 标记。

CPython 保护内部状态，应用协调完整操作。 两部分一起，构成自由线程运行所需的装甲。

***

### 三、状态

回到本章的问题：任务怎样共享状态？

一份状态只由一方使用，就留在本地；需要换手，就通过值或消息传递。 需要多方共同读取，可以让它保持不变；需要多方共同修改，就用同步规定顺序。

> 谁能看见状态，谁能修改状态，决定了谁要保护它。

真正写程序时，一个任务在哪里跑、用哪些数据、最后由谁收尾，总得一起考虑。

那么，这些关系能不能让运行时一起管起来？

***

## 第五章：自由之后——总结与展望

章节作用：总结任务结构、执行模型和状态模型，展望关系进入运行时的可能方向，并完成全场收束。

### 一、三个问题的答案

第二至第四章分别讨论了三个问题。

| 问题        | 当前答案                     |
| --------- | ------------------------ |
| 任务怎样被组织？  | 用明确的作用域管理任务的生命周期。        |
| 任务怎样真并行？  | 在进程、解释器和自由线程之间选择合适的执行模型。 |
| 任务怎样共享状态？ | 明确谁能访问和修改状态，并为共同修改安排同步。  |

它们分别处理任务的生命周期、执行位置和状态关系。

它们共同完成了一件事：

> 原来藏在全局限制里的责任，开始由更局部的结构承担。

***

### 二、展望：让关系进入运行时

现在，标准库还没有一套通用模型，能同时说明任务归谁管、用哪些状态、在哪里执行。 这些关系，还得靠我们自己接起来。

能不能由我们说清任务的要求，让运行时据此安排顺序和执行方式？

这是我的展望，还不是 CPython 已经确定的技术路线。

PEP 779 的接受说明要求核心团队开始考虑更高层的并发原语。

这说明问题已经受到关注，但具体模型仍然开放。

BOC 是其中一种探索。

在 BOC 中，Behavior 声明自己需要哪些 Cown。

需要相同资源的 Behavior 保持顺序，资源互不重叠的 Behavior 则可以并行。

`bocpy` 把这套模型带到了 CPython，使它成为可以运行和观察的实验。

它只管理被明确声明为 Cown 的资源，不能自动理解普通全局变量、数据库、文件或其他外部状态，也不提供事务回滚。

PEP 805 则从对象状态出发。 它提出 `Immutable`、`Local`、`Protected` 和 `Synchronized` 四种状态，让虚拟机在运行时检查对象能否被当前线程组访问。 局部对象还可以显式转移给另一个线程组。 这份面向 Python 3.16 的提案仍处于 Draft 状态。

其他所有权模型和声明式并发研究也在探索类似的问题。 这些方向都在尝试让原本隐含的状态与顺序关系变得可以表达，目前还没有汇成 Python 的通用答案。

Python 3.15 的相关变化只用于说明底层能力和生态仍在完善，不作为这一节的展望主体。

***

### 三、自由不是没有顺序

解除限制，并不等于删除所有顺序。

像 GIL 这样的全局限制，会让许多互不相关的 Python 线程也不能同时执行。

限制解放后，控制的目标不再是把所有工作拦在同一道门后，而是让真正有关的工作保持必要的顺序。

Lamport 的 happened-before 提供了一个简单的理解方式：有因果关系的事件必须保持先后；没有因果关系的事件不必进入同一条时间线。

TaskGroup 表达任务的父子关系，所有权和同步机制保护共享状态，BOC 则尝试让资源冲突直接形成顺序。

原来的全局装甲被拆掉以后，这些局部关系接手了它的保护作用。

> 自由并发不是没有顺序，而是不再强迫无关的事情排队。

***

### 四、回到 `for Good`

> 去年，我用一句话总结 Python 的并发道路：
>
> 杀不死我的，只会让我变得更强大。
>
> 今天，我们再往前走一步：变强以后，怎样把这份力量用好？
>
> 回到开头的 EVA 比喻：Python 的那些限制，也曾经是一层装甲。 限制可以解除，保护要由更局部的机制接手。
>
> 任务由谁收尾，数据由谁修改，哪些事情必须有先后，都要说清楚。 这样，力量释放了，程序仍然可控。
>
> 这就是今天的 `for Good`： 不只是能够并发，而是知道怎样并发得好。
>
> （切到结束页）
>
> 计算自由，数据有界。
>
> 让我们自由自在地并发。
