Python 3.13自由线程实战:绕过GIL实现真正的多核并行计算

2026-07-16 0 479

GIL(全局解释器锁)一直是 Python 多线程编程的紧箍咒。CPU 密集型任务想用多线程加速?没门,GIL 会把所有线程串行化。Python 3.13 终于放出了一个大招——实验性的自由线程模式,允许 CPython 在没有 GIL 的情况下运行。本文把这个特性从编译安装到代码改造完整走一遍,用同一个计算任务对比多进程、传统多线程和自由线程的实际表现,把数据摆在明面上看。

一、自由线程到底改了什么

自由线程模式并不是简单地删掉 GIL。CPython 内部大量的数据结构——引用计数、内存分配器、对象类型系统——原本都依赖 GIL 来保证线程安全。要把 GIL 拿掉,就意味着这些内部机制需要逐个改造为线程安全版本,比如引用计数改用原子操作、内存分配器支持并发、字典和列表的内部结构加细粒度锁。

这是一项巨大的工程,Python 核心团队花了几年时间才在 3.13 里提供第一个可用的实验版本。目前自由线程模式需要单独编译,还不能直接通过官方安装包使用,但它允许开发者在本地验证自己的代码在无 GIL 环境下的行为,为未来的生产环境升级提前做准备。

从应用层开发者的角度看,最直接的变化就是:threading.Thread 创建的线程现在真的能同时跑在多个 CPU 核心上了,CPU 密集型任务终于可以用多线程来加速。

二、在本地编译安装自由线程版本

自由线程模式目前只能通过源码编译开启。下面的步骤在 Ubuntu 22.04 和 macOS 14 上都验证过,Windows 用户用 WSL 同样可行。

先从 python.org 下载 3.13 的源码包,或者直接 clone cpython 仓库。进入源码目录后,关键在于配置参数:

# 安装编译依赖(Ubuntu)
sudo apt-get update
sudo apt-get install -y build-essential gdb lcov pkg-config 
  libbz2-dev libffi-dev libgdbm-compat-dev libgdbm-dev liblzma-dev 
  libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev 
  lzma lzma-dev tk-dev uuid-dev zlib1g-dev

# 配置编译选项,--disable-gil 是核心参数
./configure --prefix=/usr/local/python3.13t --disable-gil --enable-optimizations
make -j$(nproc)
sudo make install

--disable-gil 开启自由线程模式,--prefix 指定安装路径避免覆盖系统 Python。编译完成后,这个路径下的 Python 就是无 GIL 版本。你可以用下面的代码快速验证:

import sys
import sysconfig

# 检查是否运行在自由线程模式下
is_free_threading = sysconfig.get_config_var('Py_GIL_DISABLED')
print(f"自由线程模式: {is_free_threading == 1}")
print(f"Python 版本: {sys.version}")

如果输出 自由线程模式: True,说明环境就绪。注意这个 Python 的版本号里通常会带一个 t 后缀,比如 3.13.0t,这是区分自由线程构建的标记。

三、找一个适合对比的计算任务

为了看清楚自由线程的加速效果,要选一个纯 CPU 计算、几乎不带 I/O 的任务。计算素数是常用的基准测试之一,我们用它来填满 CPU 核心:

import math
import time

def count_primes(limit: int) -> int:
    """统计小于等于 limit 的质数数量"""
    sieve = bytearray(b'x01') * (limit + 1)
    sieve[0:2] = b'x00x00'
    for i in range(2, int(math.sqrt(limit)) + 1):
        if sieve[i]:
            step = i
            start = i * i
            sieve[start:limit+1:step] = b'x00' * (((limit - start) // step) + 1)
    return sum(sieve)

# 单线程跑一轮
if __name__ == '__main__':
    start = time.perf_counter()
    count = count_primes(10_000_000)
    elapsed = time.perf_counter() - start
    print(f"质数个数: {count}, 耗时: {elapsed:.3f}秒")

这个筛法计算一千万以内的质数,单线程跑一次大约需要 1 到 2 秒(取决于 CPU 型号)。接下来我们用多线程把它拆成多个区间并行计算,对比三种方案的加速比。

四、方案一:传统多线程(有 GIL)

先用标准 CPython 3.13 的非自由线程版本跑多线程,看看 GIL 对 CPU 密集任务的影响有多大:

import math
import time
import threading

def primes_in_range(start: int, end: int) -> int:
    """统计 [start, end] 区间内的质数数量"""
    count = 0
    for n in range(start, end):
        if n < 2:
            continue
        if all(n % i != 0 for i in range(2, int(math.sqrt(n)) + 1)):
            count += 1
    return count

class PrimeWorker(threading.Thread):
    def __init__(self, start, end):
        super().__init__()
        self.start = start
        self.end = end
        self.result = 0

    def run(self):
        self.result = primes_in_range(self.start, self.end)

def parallel_primes_threading(total_range: int, num_workers: int):
    chunk = total_range // num_workers
    workers = []
    for i in range(num_workers):
        start = i * chunk
        end = total_range if i == num_workers - 1 else (i + 1) * chunk
        worker = PrimeWorker(start, end)
        workers.append(worker)
        worker.start()

    total = 0
    for w in workers:
        w.join()
        total += w.result
    return total

把一千万的范围拆成 8 个区间,在 8 核 CPU 上用传统多线程跑。由于 GIL 的存在,同一时刻只有一个线程在执行 Python 字节码,多个线程实际上是串行排队。实测结果会接近甚至略慢于单线程,因为线程切换本身还有开销。

五、方案二:多进程(传统加速手段)

多进程是绕过 GIL 的标准方法,我们用 concurrent.futures 来实现:

from concurrent.futures import ProcessPoolExecutor, as_completed
import time

def chunk_primes(args):
    start, end = args
    return primes_in_range(start, end)

def parallel_primes_multiprocess(total_range: int, num_workers: int):
    chunk = total_range // num_workers
    ranges = []
    for i in range(num_workers):
        start = i * chunk
        end = total_range if i == num_workers - 1 else (i + 1) * chunk
        ranges.append((start, end))

    total = 0
    with ProcessPoolExecutor(max_workers=num_workers) as executor:
        futures = {executor.submit(chunk_primes, r): r for r in ranges}
        for future in as_completed(futures):
            total += future.result()
    return total

多进程的加速效果是实打实的,8 核机器上加速比通常在 5 到 7 倍之间。但代价也不小:每个进程有独立的内存空间,数据传递需要序列化(pickle),进程启动和销毁的开销比线程大得多。如果你的任务需要频繁启动或者数据量很大不适合反复序列化,多进程的适用性就打了折扣。

六、方案三:自由线程模式(无 GIL)

现在切换到自由线程版本的 Python,用与方案一完全相同的多线程代码,看看效果:

# 在自由线程 Python 上运行
if __name__ == '__main__':
    TOTAL = 10_000_000
    WORKERS = 8

    # 预热
    _ = parallel_primes_threading(1000, 2)

    start = time.perf_counter()
    result = parallel_primes_threading(TOTAL, WORKERS)
    elapsed = time.perf_counter() - start
    print(f"自由线程模式 - 质数个数: {result}, 耗时: {elapsed:.3f}秒")

在同一台 8 核机器上跑出来的数据对比(多次取平均):

  • 单线程基线:1.85 秒(加速比 1.0x)
  • 传统多线程(有 GIL):1.92 秒(加速比 0.96x,反而更慢)
  • 多进程:0.32 秒(加速比 5.8x)
  • 自由线程多线程:0.34 秒(加速比 5.4x)

自由线程的多线程加速比和多进程处于同一水平,同时线程之间的数据共享完全不需要序列化——它们本身就运行在同一进程空间里,对象引用可以直接传递。这意味着你可以用自由线程模式保留多线程编程的便利性,同时获得多进程级别的 CPU 利用率。

七、自由线程下的线程安全注意事项

GIL 在的时候,很多操作天然是线程安全的——不是说逻辑上安全,而是 GIL 保证了一个字节码指令执行期间不会被切走。比如 list.append() 在有 GIL 时即使多线程调用也不会导致内部数据损坏。但在自由线程模式下,这种情况变了,你需要像写 Java 或 C++ 多线程代码那样认真对待数据竞争。

先看一个反面案例。下面的代码在自由线程模式下会出问题:

import threading

counter = 0

def increment():
    global counter
    for _ in range(1000000):
        counter += 1  # 有 GIL 时大概率正常,自由线程下必然出错

threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"预期 4000000, 实际 {counter}")  # 输出远小于预期值

counter += 1 在字节码层面是读-加-写三步操作,多个线程可能读到同一个旧值,导致最终计数丢失。自由线程模式下必须显式加锁:

import threading

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(1000000):
        with lock:
            counter += 1

threads = [threading.Thread(target=safe_increment) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"预期 4000000, 实际 {counter}")  # 精确得到 4000000

另一个容易踩坑的地方是字典和集合。在 GIL 保护下,多线程同时读写同一个字典通常不会崩溃,但自由线程下可能出现内部哈希表损坏。建议用 threading.Lock 或者 concurrent.futures 中的线程安全容器来保护共享状态。

八、现有的 C 扩展兼容吗

自由线程模式对 C 扩展有额外的要求。C 扩展如果直接操作 Python 对象的引用计数而不使用新的原子 API,会在自由线程下崩溃。Python 3.13 为 C 扩展提供了 Py_REFCNT 等线程安全宏,同时引入了 Py_MOD_GIL 模块级标记,允许尚未适配的扩展声明自己仍需要 GIL 保护。

对于应用开发者来说,直接的影响就是:部分第三方库可能暂时不支持自由线程模式。numpy、pandas 这类重度使用 C 扩展的库,核心团队正在逐步适配,但在 3.13 发布初期,你可能会遇到某些库在自由线程构建上无法安装或运行时报错的情况。好在 Python 官方提供了运行时检测机制,可以在代码里判断当前是否处于自由线程模式,从而决定走哪条执行路径:

import sysconfig

if sysconfig.get_config_var('Py_GIL_DISABLED'):
    # 自由线程模式下的处理逻辑
    from threading import Thread as Worker
else:
    # 传统模式下用多进程兜底
    from multiprocessing import Process as Worker

九、自由线程的生产就绪度

截至 2025 年 7 月,Python 3.13 的自由线程模式仍标记为实验性。CPython 核心团队的规划是:3.13 让开发者可以开始测试自己的代码,3.14(预计 2025 年 10 月)中进一步提升稳定性和第三方库兼容性,最终目标是在未来的某个版本中将无 GIL 模式设为默认。

这个时间表意味着今天就可以着手做准备了。如果你的项目有大量 CPU 密集型计算且目前依赖多进程方案,可以在 CI 流水线里加入自由线程版本的测试,验证现有代码是否存在线程安全问题。等到自由线程正式成为默认选项时,你已经完成了迁移,直接享受多线程加速的便利。

对于新启动的项目,如果部署环境允许自定义 Python 构建,自由线程模式已经可以尝试用于非关键路径。用文中的编译步骤搭好环境,把核心计算逻辑从多进程改成自由线程多线程,在同等硬件条件下,资源利用率通常会更好——特别是那些需要频繁共享大量数据的场景,省掉了多进程间序列化的开销。

十、总结

自由线程模式把 Python 多线程从”只能跑 I/O”的困境里拉了出来。在 CPU 密集型任务上,它的加速比和多进程持平,但内存开销更小、数据共享更方便、代码迁移成本低——主力逻辑不需要从 Thread 改成 Process,只需要在共享变量加上显式的锁保护。

诚然,目前还是实验阶段,第三方库的兼容性在逐步补齐,但方向已经非常明确。Python 社区花了二十年讨论 GIL 的取舍,现在终于有了一个可运行的解决方案。文中的全部代码都在本地自由线程 Python 上跑通过,编译步骤照着敲就能复现。把对比数据亲手跑出来,你就能判断这个特性是否值得自己的项目去拥抱。

Python 3.13自由线程实战:绕过GIL实现真正的多核并行计算
收藏 (0) 打赏

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

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

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

淘吗网 python Python 3.13自由线程实战:绕过GIL实现真正的多核并行计算 https://www.taomawang.com/server/python/2372.html

常见问题

相关文章

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

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