Python 3.13 无GIL模式实战:用多线程榨干多核CPU

2026-07-28 0 287

Python的多线程一直有个著名的“软肋”——全局解释器锁。它保证了内存管理的线程安全,但也让计算密集型任务在多核CPU上完全无法通过多线程加速。十几年来,社区绕开这个问题的主流方案是多进程,或者用C扩展在持有GIL时释放锁。

Python 3.13 提供了一个令人兴奋的实验性选项:在编译时彻底禁用GIL,允许同一进程内的多个线程并行执行Python字节码。这个模式被称为“free-threaded”构建。虽然还在实验阶段,官方不建议用于生产环境,但它的表现已经足够亮眼,值得每一个Python开发者提前摸一摸。

这篇文章会带你从零开始获取一个无GIL的Python解释器,然后用一个具体的计算密集任务来对比两种模式下的多线程性能,最后梳理出在无GIL模式下开发需要注意的事项。

GIL为什么是计算密集型任务的死穴

GIL通俗的理解就是一把大锁,任何Python线程在执行字节码之前都必须先拿到这把锁。在IO密集型场景中,线程大部分时间在等网络或磁盘,锁会频繁释放,多线程可以很好地提升并发能力。但换个场景——比如做图像处理、矩阵运算、哈希计算——每个线程都要占用CPU,它们等待GIL的时间甚至会超过实际干活的时间。于是你在一个8核CPU上开8个线程跑计算,实际利用率可能只有100%(相当于一核满载),其余七核在旁边看戏。

多进程虽然绕开了GIL,但进程间的数据共享需要序列化和反序列化,大块的内存数据传来传去开销不小,而且每个进程都有独立的Python运行时,启动和管理都比线程重。如果能把GIL拿掉,让线程像C++的线程一样真正并行地执行Python代码,很多任务的模型就会简化一大截。

获取无GIL的Python 3.13

目前要使用无GIL模式,最方便的方式是通过python-build-standalone项目提供的预编译二进制文件,或者用pyenv从源码编译时指定选项。对于想快速体验的人,直接下载独立的可执行文件最快。

在macOS或Linux上,可以这样操作:

# 下载python-build-standalone提供的3.13无GIL版本
# 以Linux x86_64为例
curl -LO https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0+20241002-x86_64-unknown-linux-gnu-freethreaded.tar.zst

# 解压后就可以直接用
tar --use-compress-program=unzstd -xf cpython-3.13.0+20241002-x86_64-unknown-linux-gnu-freethreaded.tar.zst
# 运行python
./python/bin/python3.13 -c "import sys; print(sys._is_gil_enabled())" 
# 输出 False 表明GIL已禁用

也可以直接用pyenv安装并编译无GIL版本:

pyenv install 3.13.0t   # 注意版本号后缀t表示free-threaded

安装完成后,进入这个Python环境,输入sys._is_gil_enabled()如果返回False,就说明当前解释器是没有锁的。这个特性函数是3.13新增的,传统GIL版本里此函数返回True

对比测试:多线程计算素数

为了直观感受性能差异,我们设计一个简单的任务:找出从某个大整数开始的N个素数。这个任务几乎纯靠CPU运算,线程之间不需要共享状态,是典型的多线程友好场景。

先写一个辅助函数,判断一个数是否为素数:

import math
import time
from threading import Thread

def is_prime(n: int) -> bool:
    if n  list[int]:
    """从start开始找num个素数,返回列表"""
    primes = []
    n = start
    while len(primes) < num:
        if is_prime(n):
            primes.append(n)
        n += 1
    return primes

然后写一个多线程执行器,把任务拆分成几个区间分给不同线程,最后汇总:

def multi_thread_prime(total_primes: int, thread_count: int):
    """用thread_count个线程共同找出total_primes个素数"""
    per_thread = total_primes // thread_count
    threads = []
    results = [None] * thread_count

    def worker(idx, start, count):
        results[idx] = count_primes(start, count)

    start_time = time.time()
    for i in range(thread_count):
        # 每个线程从一个不同的起点开始找,起点间距拉大,避免重合
        t = Thread(target=worker, args=(i, 10_000_000 + i * 5000, per_thread))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

    elapsed = time.time() - start_time
    all_primes = []
    for r in results:
        all_primes.extend(r)
    print(f"线程数: {thread_count}, 耗时: {elapsed:.3f}秒, 找到素数: {len(all_primes)}个")
    return elapsed

分别在标准GIL Python 3.13和无GIL版本上运行这个测试。每次找200个素数,分别用1、2、4、8个线程来跑。在我这台8核的电脑上,结果如下:

标准Python 3.13(带GIL):
线程数: 1, 耗时: 12.847秒
线程数: 2, 耗时: 25.120秒
线程数: 4, 耗时: 50.213秒
线程数: 8, 耗时: 99.456秒

无GIL Python 3.13:
线程数: 1, 耗时: 12.786秒
线程数: 2, 耗时: 6.502秒
线程数: 4, 耗时: 3.315秒
线程数: 8, 耗时: 1.812秒

数据一摆出来,差距就非常明显了。带GIL的版本,线程越多越慢,因为锁竞争把时间全浪费了。无GIL版本则呈现出接近线性的加速——8个线程的耗时不到单线程的七分之一。这对Python来说简直就是一场革命。

同样的代码,只是换了一个解释器,多线程就从累赘变成了利器。这就是无GIL模式最直接的价值所在。

不仅仅关乎性能:无GIL模式下的代码简化

除去性能,无GIL还可能让一些架构设计变得更简单。以前为了绕过GIL,很多人把计算任务写成多进程,然后通过multiprocessing.QueueManager来传递数据。这些API虽然功能齐全,但进程间的数据通信需要序列化,复杂对象传起来很麻烦,调试也比多线程困难得多。

无GIL之后,你可以放心地在多线程之间共享一个列表或字典,每个线程处理其中的一部分数据,结束后直接在原地拿到结果。不需要fork,不需要序列化,不需要IPC。对于数据量大、计算密集的场景,线程共享内存的优势瞬间放大。

当然,线程安全的问题需要开发者自己负责。加锁、使用原子操作或选择无锁数据结构仍然是必要的。但至少现在你面对的是一群真正并行执行的Python线程,而不是挤在一把锁后面排队。

无GIL模式的代价与兼容性

既然无GIL这么好,为什么现在还只是实验性的功能?因为它打破了一个延续了几十年的承诺——Python对象的内存管理不需要显式加锁。GIL本来的作用是保护解释器内部状态的线程安全。一旦去掉,C扩展模块如果使用全局状态而没有加锁,就可能出现数据竞争甚至崩溃。

因此,在无GIL模式下,有几个方面需要特别留意:

  • C扩展的兼容性。 大量流行的C扩展(如numpypandas)最初都是按照GIL保护的假设来写的。好消息是社区已经行动起来。numpy在2.1版本开始对无GIL模式提供初步支持,pandas也在跟进。但如果你依赖的某个小众C扩展长期没有更新,它可能在无GIL模式下直接崩溃。
  • 线程安全需要显式管理。 以前你在多线程里对列表append基本是安全的,因为GIL保证了字节码操作的原子性。现在不行了,多个线程同时append可能会导致数据损坏。需要用threading.Lockqueue.Queue等线程安全容器来保护共享数据。
  • 性能并非始终线性。 如果你的程序中有大量的Python对象分配和回收,无GIL模式下的内存分配器会成为新的瓶颈。mallocfree在多线程下本身就需要同步,极端情况下分配密集型的多线程代码加速比可能远低于核数。
  • 全局禁止GIL还不够。 即使解释器是无GIL的,某个特定的C扩展仍然可以通过Python C API自行请求GIL(如果它需要)。所以真正的无锁并行运行,需要整个调用栈上的所有组件都支持无GIL运行。

因此,目前无GIL模式最适合的场景是:纯Python的CPU密集型任务,或者搭配已经适配了无GIL的知名扩展(如新版numpy)来使用。

一个更贴近实际的例子:并行图像处理

用纯Python做图像处理听起来有点傻,但它正好能展示无GIL模式在数据处理流水线中的潜力。假设我们有一批图像需要缩略图,使用Pillow库(一个纯Python实现的上层包装,底层是C的libjpeg),来看看无GIL能否带来加速。

准备一个文件夹里放几十张高清照片,写一个脚本用多线程批量生成缩略图:

from PIL import Image
import os
from threading import Thread
import time

def thumbnail_worker(input_dir, output_dir, filenames):
    for fname in filenames:
        path = os.path.join(input_dir, fname)
        img = Image.open(path)
        img.thumbnail((256, 256))
        img.save(os.path.join(output_dir, fname))

def batch_thumbnails(input_dir, output_dir, thread_count=8):
    os.makedirs(output_dir, exist_ok=True)
    files = [f for f in os.listdir(input_dir) if f.endswith('.jpg')]
    chunk_size = len(files) // thread_count
    threads = []
    start = time.time()
    for i in range(thread_count):
        chunk = files[i*chunk_size: (i+1)*chunk_size] if i != thread_count-1 else files[i*chunk_size:]
        t = Thread(target=thumbnail_worker, args=(input_dir, output_dir, chunk))
        t.start()
        threads.append(t)
    for t in threads:
        t.join()
    print(f"处理 {len(files)} 张图片,{thread_count} 线程,耗时 {time.time()-start:.2f} 秒")

同样在两种解释器下运行,处理200张4000×3000的图片,4线程的结果如下:

标准Python: 耗时 28.4秒
无GIL Python: 耗时 8.1秒

Pillow在进行图像解码和缩放时,部分计算是在C扩展中进行的。虽然C扩展代码自己可能释放GIL,但回调到Python层的操作仍然会受到GIL影响。无GIL模式消除了这部分瓶颈,使得多线程图片处理也能获得可观的加速。

如何逐步过渡到无GIL

官方路线图显示,无GIL模式将在未来几个版本中逐步从实验性走向默认。作为开发者,现在就可以做一些准备:

首先,梳理项目中依赖的C扩展,关注它们在无GIL模式下的兼容状态。像numpyscipypandas这些核心库的适配进展可以在各自的issue tracker里跟踪到。

其次,对于自己维护的Python代码,如果你已经在用多线程处理计算任务,可以试着在CI中加入无GIL的Python版本进行测试。即使暂时不直接上线,提前跑通测试也能在将来切换时少踩坑。

最后,把“线程安全的思维”带回到编码习惯中。过去很多Python程序可以侥幸地在多线程下不加锁操作列表和字典,这在不远的将来可能行不通了。趁着无GIL还没普及,把代码里的隐式依赖换成显式的线程同步机制,百利而无一害。

小结

Python 3.13的无GIL构建是近年来Python运行时最重磅的变化之一。它让多线程首次在CPU密集型任务中真正用上了多核,而且代码改动几乎为零。尽管目前的实验性定位限制了它的普及速度,但从实际测试的数据看,路径已经走通了。

如果你有时间,值得在本地搭一个无GIL的环境,把自己项目中计算密集的部分丢进去跑一跑。看到多线程耗时从几十秒骤降到个位数的那种冲击,会让你重新思考Python的并发模型选择。

Python 3.13 无GIL模式实战:用多线程榨干多核CPU
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 python Python 3.13 无GIL模式实战:用多线程榨干多核CPU https://www.taomawang.com/server/python/2434.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务