过去提到Python的多线程,大家第一反应就是“有GIL锁,CPU密集型任务没戏”。所以我一直老老实实用multiprocessing绕开这个坎,直到前阵子看到Python 3.13的发布说明里多了一个自由线程模式(free-threading),官方叫“3.13t”,也就是不带GIL的构建版本。
看完我就坐不住了,周末特意腾出时间折腾了一下。毕竟,如果这玩意儿真有那么神奇,以后写并行代码可能就不需要再序列化数据、用ProcessPoolExecutor那一套了。
这个“自由线程”到底是什么
简单说,Python 3.13开始提供了一种特殊构建,叫做“free-threaded build”,在这个版本里,解释器禁用了全局解释器锁。也就是多个线程可以真正同时在多个CPU核心上执行Python字节码。
注意,它不是运行时给你一个开关,而是相当于编译了一个没有GIL的Python解释器。你需要单独安装二进制或者自己编译。在官方下载页面或者通过python.org的安装包可以选择“3.13t”版本。目前Windows、macOS和Linux都有对应的安装包。
我是在Linux上测试的,直接下载了python3.13t的二进制,解压以后是一个独立的环境,不会干扰系统里原来的Python。
wget https://www.python.org/ftp/python/3.13.0/Python-3.13.0.tar.xz tar -xf Python-3.13.0.tar.xz cd Python-3.13.0 ./configure --disable-gil make -j$(nproc) sudo make install
编译过程大概花了几分钟。安装好之后,你的系统里会多一个python3.13t可执行文件。运行一下看看版本:
python3.13t -V # Python 3.13.0 experimental free-threading build
看到“experimental free-threading build”就说明成功了。
写段代码验证奇迹还是幻觉
我写了一个比较常见的计算密集任务——计算从1到某个数的平方和。为了方便测试多线程并行,我把它拆成四个子任务,分别交给线程池处理。
import threading
import time
from concurrent.futures import ThreadPoolExecutor
def heavy_calc(start, end):
total = 0
for i in range(start, end):
# 故意做一些无意义的运算
total += i * i % 1000000
return total
def run_multi_thread():
ranges = [
(1, 2500000),
(2500000, 5000000),
(5000000, 7500000),
(7500000, 10000000),
]
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as pool:
futures = [pool.submit(heavy_calc, r[0], r[1]) for r in ranges]
for f in futures:
f.result()
return time.perf_counter() - start
if __name__ == "__main__":
print("耗时:", run_multi_thread())
如果是普通版的Python 3.12,跑这个代码我预计会非常慢,因为四个线程抢一把锁,时间主要花在切换等待上。而在自由线程版解释器上,四个线程理论上可以各跑各的。
测试结果:令人意外的提升
我先用系统自带的Python 3.12(有GIL)跑了一遍,然后切到python3.13t跑同样的代码。结果如下:
# 普通 Python 3.12 耗时: 1.862 秒 # 自由线程 Python 3.13t 耗时: 0.492 秒
自由线程版本足足快了3.7倍左右,接近四核并行的理想值。这还只是把任务切成四份。如果差更多,效果会更夸张。
等一下,我知道你可能会问:会不会是因为Python 3.13本身比3.12更快?我也做了个对照实验——在同样的自由线程解释器下,把任务的max_workers改成1。结果单线程耗时是1.9秒,跟普通版的单线程差不多。所以提速不是版本优化带来的,而是因为多线程真的并行执行了。
那一刻,我怎么感觉有点想哭——十几年啊,CPython终于他妈的有种了。
但是,先别急着把multiprocessing全删了
自由线程目前还是“实验性”阶段。我之前看到官网上写过这样一句:这个模式会牺牲掉一些单线程性能,因为申请内存、引用计数保护都要额外开销。实测普通版本处理同样的纯计算需求,比3.13t单线程快一丢丢(大概5%~10%)。也许这就是去掉GIL需要付出的代价。
如果你现在的项目是纯Python代码,且没有太多无法绕过的C扩展依赖,那可以考虑尝试。但是很多常用库,比如numpy、pandas、cryptography,它们内部的C扩展是不是线程安全?这个还不敢保证。官方文档也说,需要这些库针对自由线程构建做适配,或者通过给每个线程加锁来保护内部状态。
我试着在自由线程解释器里导入numpy,直接崩了,提示只支持非自由线程版。所以实际生产项目还是得老老实实等主流库适配。
用text with real code 跑一个更实际的场景
纯数学计算有点无聊,我换了个贴近业务的东西——对一堆字符串文件做词频统计。比如同时读四个文件,每个文件大概几万行,统计每个文件的单词数,然后汇总。用线程池跑四个文件,每个线程读取并解析一个文件。
import threading
from concurrent.futures import ThreadPoolExecutor
def count_words(filename):
count = 0
with open(filename, 'r', encoding='utf-8') as fp:
for line in fp:
count += len(line.split())
return count
def run_with_threads(files):
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(count_words, files))
return sum(results)
if __name__ == '__main__':
files = ['data1.txt', 'data2.txt', 'data3.txt', 'data4.txt']
print(run_with_threads(files))
这个场景算是“混合”的:文件读取时会有一些系统IO,但split和计数则是纯CPU工作。在普通线程版里,因为GIL的限制,多线程甚至可能比单线程还要慢,因为每次开关锁也有开销。但在3.13t版本上,文件读取和解析能并行,跑完确实快,而且写起来也简单,不需要担心传大文件队列给子进程的麻烦。
线程还是不能无限开
自由线程不代表你可以无限制地创建线程,它只是去掉了GIL,JVM式的调度,但Python虚拟机的运行时本身仍然会做一些原子操作保护,比如分配内存的时候内部有锁。当你开启成千上万个线程做纯计算,线程切换和内存分配抢占也会成为瓶颈。
我在16核的机器上试过16个线程,效率最高。改成64个线程,速度反而变慢,因为CPU核心数只有那么多,线程上下文切换开销开始占上风。所以用了自由线程,照样需要合理地控制线程数量。
什么时候能等来“默认就无GIL”
Python官方团队已经在规划Python 3.14或3.15里考虑把自由线程作为默认特性吗?实际上Python的指导委员会还在评估。自由线程构建目前是一个独立功能,需要额外编译。也许再过两三个小版本,官方会把它变为标准构建的一部分。但兼容性问题太多,哪怕是标准类型list和dict,底下也加了细粒度的锁。这些锁在某些情况下会产生死锁,所以他们必须非常谨慎。
不过说实话,作为一个日常拿Python写爬虫和并行脚本的人,能看到官方朝着这个方向迈出实质性一步,心里是非常激动的。至少意味着未来确实有希望告别ProcessPoolExecutor的酸爽(你得序列化所有参数,然后还得处理if __name__ == ‘__main__’)。
小结:该不该现在就用
我的建议很明确:
- 如果你只是好奇或者想做实验,放心大胆下载3.13t体验一下,它不会弄坏你系统里的原版Python。
- 如果是写生产用的服务或者小工具,暂时不要在生产环境使用自由线程版。别贪图这一时爽,插件库不兼容会让你脑壳疼。
- 如果你维护的是纯Python计算模块,且没有太多第三方C扩展,你可以悄悄在非关键任务中用自由线程模式来加速,记得做好回归测试。
抛开GIL之后,Python的多线程编程模式也就没什么可怕的了。线程之间依然要面对竞态条件,但至少不会因为一个全局锁而白白浪费CPU。希望在不久的将来,我能用普通的python3命令直接跑满多核线程,不用再费劲搞多进程。
最后放一句我自己改的项目里看到的话:我们不需要再问“GIL什么时候走”,因为它正在悄悄退场,只是还需要一点时间。

