For the complete documentation index, see llms.txt. This page is also available as Markdown.

让我们自由自在地并发

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 3.11 的 asyncio.TaskGroup 把子任务收进明确的作用域。

离开 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 中提供浅层不可变的映射。 但要留意"浅层":映射里的键值对应关系不能改,值如果是列表,列表里的内容仍然可以变。


二、同步

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

竞态

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

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

应用里的缓存需要协调,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: 不只是能够并发,而是知道怎样并发得好。

(切到结束页)

计算自由,数据有界。

让我们自由自在地并发。

最后更新于