
Python GIL 深度解析它锁住了谁又究竟保护了什么提到 Python 多线程GIL 几乎是一个绕不开的话题。很多刚接触 Python 并发编程的开发者都听过这样一句话“Python 有 GIL所以 Python 多线程没有用。”也有人会进一步理解成“既然有 GILPython 多线程就是线程安全的。”遗憾的是这两句话都不准确。GIL 确实会限制传统 CPython 中多个线程同时执行 Python 代码但它并没有让threading失去价值更重要的是GIL 保护的主要对象并不是你的订单、余额、缓存这些业务数据而是 Python 解释器自身以及 Python 对象内部状态的一致性。如果没有理解这一点在项目中很容易写出这样的代码balance1000# 有 GIL所以多线程访问 balance 应该安全吧然后在生产环境里得到一个非常昂贵的答案不安全。本文就来系统拆解这个 Python 编程中经常被误解的概念GIL 是什么GIL 为什么会存在它真正保护的是什么为什么有 GIL 仍然会出现线程安全问题为什么 I/O 密集程序仍然适合多线程为什么 CPU 密集任务通常不适合传统 CPython 多线程NumPy 为什么又能利用多核Python 3.13、3.14 的 free-threaded Python 又意味着什么理解完这些问题你对 Python 并发编程的认识会清晰很多。一、GIL 到底是什么GIL 全称Global Interpreter Lock中文一般翻译为全局解释器锁。对于传统启用 GIL 的 CPython可以把它粗略理解为一把解释器级别的大锁CPython 进程 Thread A ───────┐ Thread B ───────┼──→ [ GIL ] ──→ Python 对象 / Python C API Thread C ───────┘同一时刻通常只有一个线程能够持有 GIL并执行相应的 Python 代码、访问 Python 对象和调用需要 GIL 的 Python C API。Python 官方 C API 文档对此给出的核心解释非常明确传统 CPython 本身并不是天然线程安全的因此线程在访问 Python 对象之前需要持有 GIL。(Python documentation)所以 GIL 首先是一个CPython 解释器内部的同步机制。这里一定要注意一个关键词CPython。Python 是一门语言而 CPython 是 Python 最主流的实现。因此说Python 语言规定所有实现都必须有 GIL是不准确的。更严谨的说法应该是我们平时讨论的 GIL主要讨论的是 CPython 的实现机制。二、GIL 为什么会存在要理解这个问题需要先看到 CPython 内部非常重要的一套机制引用计数CPython 对象管理大量依赖引用计数。例如importsys data[]print(sys.getrefcount(data))假设adata那么data又多了一个引用。粗略理解对象 data │ ├── 变量 data └── 变量 a 引用数量增加当对象引用计数最终降到 0 时CPython 通常就可以释放它。在 C 层面可以抽象成ob_refcnt 1和ob_refcnt - 1问题来了。假如没有任何同步机制两个线程同时对同一个 Python 对象的引用计数进行修改原引用计数 10 Thread A 读取10 Thread B 读取10 Thread A 写入11 Thread B 写入11理论上应该变成12结果却可能得到11如果这种错误发生在对象生命周期管理上后果就不仅仅是“数字算错了”。它可能意味着对象被提前释放 对象迟迟不能释放 内存状态损坏 解释器崩溃Python 官方文档也正是用“两个线程同时增加同一对象引用计数”为例解释为什么传统 CPython 需要 GIL。(Python documentation)所以 GIL 的历史价值非常实际用一把全局锁让大量解释器内部操作不需要分别设计复杂的细粒度并发控制。三、GIL 真正保护的到底是什么这是本文最核心的问题。很多开发者误以为GIL 保护 Python 变量。严格来说不是。更准确地说传统 CPython 中的 GIL主要帮助保护的是1. CPython 解释器内部状态解释器执行 Python 代码过程中需要不断操作对象 引用计数 类型信息 内存管理结构 执行状态 C API这些结构如果被多个线程完全无约束地同时修改就需要大量额外同步机制。2. Python 对象的底层内部一致性例如my_list.append(value)涉及的不只是“向列表添加一个元素”。底层还可能涉及容量检查 重新分配空间 保存对象指针 修改列表长度 修改引用计数GIL 使传统 CPython 中很多底层操作天然处于一个较大的串行执行区域内。3. Python/C API 的访问安全对于 C 扩展开发者来说这一点尤其重要。传统 CPython 中如果线程要操作PyObject*或者调用大量 Python C API一般必须拥有相应的线程状态并持有 GIL。因此可以把 GIL 想象成GIL │ ▼ ┌──────────────────────────┐ │ CPython Interpreter │ │ │ │ reference counting │ │ Python objects │ │ memory/object state │ │ Python C API │ └──────────────────────────┘但是注意GIL ≠ 你的业务锁这个区别至关重要。四、最危险的误区有 GIL就不需要 Lock当然不是。假设我们开发一个简单的账户系统balance100现在两个线程分别尝试扣款 80 元。代码importtimeimportthreading balance100defwithdraw(amount):globalbalanceifbalanceamount:time.sleep(0.01)balance-amount t1threading.Thread(targetwithdraw,args(80,))t2threading.Thread(targetwithdraw,args(80,))t1.start()t2.start()t1.join()t2.join()print(最终余额,balance)你可能期望20因为账户只有 100 元不应该允许两次 80 元扣款同时成功。但两个线程可能这样运行balance 100 Thread A: 检查 100 80 结果True ↓ 切换线程 Thread B: 检查 100 80 结果True Thread B: 100 - 80 20 ↓ 切换线程 Thread A: 20 - 80 -60最终balance -60GIL 去哪里了GIL 一直都在。问题在于GIL 没有承诺你整个业务逻辑是一笔不可分割的事务。这段业务代码包含多个步骤读取余额 ↓ 判断余额 ↓ 执行其他操作 ↓ 修改余额线程完全可能在这些步骤之间发生切换。所以这里真正需要的是lockthreading.Lock()完整写法importthreading balance100lockthreading.Lock()defwithdraw(amount):globalbalancewithlock:ifbalanceamount:balance-amount threads[threading.Thread(targetwithdraw,args(80,))for_inrange(2)]forthreadinthreads:thread.start()forthreadinthreads:thread.join()print(balance)这时withlock:保护的是你的业务临界区。因此一定要记住GIL → 主要保护解释器内部一致性 Lock → 保护应用程序共享状态的一致性两者解决的不是同一个层面的问题。五、为什么 GIL 不等于“一个 Python 操作就是原子的”再看一个常见代码counter1表面看只有一行。但“一行 Python 源代码”并不意味着底层只执行一步。可以自己观察字节码importdis counter0defincrease():globalcounter counter1dis.dis(increase)不同 Python 版本生成的具体字节码可能不同但核心思想不会变读取 counter 执行加法 保存结果因此counter1不能简单理解为因为有 GIL所以它天然就是我的业务原子操作。更危险的是类似ifkeynotincache:cache[key]load_data()即使某些单独的dict操作在特定 CPython 实现下具有内部安全特征这一整段判断 计算 写入仍然是组合操作。两个线程完全可能同时发现key 不存在然后各执行一次load_data()所以线程安全设计的原则应该是不要依赖“这几行代码看起来很短”而要分析共享状态的完整不变量。六、既然有 GILPython 多线程还有什么意义非常有意义。关键在于区分两种任务CPU Bound I/O Bound即CPU 密集型 I/O 密集型七、I/O 密集任务多线程依然非常实用假设程序执行responserequests.get(url)真正耗费的大部分时间可能不是 CPU 计算而是在等待DNS 网络 服务器响应 磁盘 Socket例如Thread A 发送请求 ↓ 等待网络........................此时让 CPU 一直闲着没有意义。CPython 会在许多阻塞 I/O 场景释放 GIL从而让其他线程获得执行机会。官方文档也明确指出GIL 会在文件读取、写入等阻塞 I/O 周围释放。(Python documentation)所以Thread A等待网络 Thread B处理响应 Thread C等待数据库 Thread D发送请求能够形成很好的并发效果。简单案例fromconcurrent.futuresimportThreadPoolExecutorimportrequests urls[https://example.com,https://example.org,https://python.org,]defdownload(url):responserequests.get(url,timeout10)returnurl,len(response.content)withThreadPoolExecutor(max_workers8)asexecutor:resultsexecutor.map(download,urls)forresultinresults:print(result)对于HTTP 请求 数据库访问 文件 I/O Socket 通信 第三方 API线程池仍然是非常实用的工具。因此正确结论不是Python 有 GIL所以不能使用线程。而应该是传统 CPython 的线程通常特别适合 I/O 密集型并发但纯 Python CPU 密集任务很难通过普通线程充分利用多个 CPU 核心。Python 官方threading文档也给出了相同方向的建议。(Python documentation)八、CPU 密集任务为什么线程通常加速不了假设defcpu_work(n):total0foriinrange(n):totali*ireturntotal这里几乎没有等待。CPU 一直执行 Python 代码。如果创建四个线程Thread 1 ─┐ Thread 2 ─┤ Thread 3 ─┼──→ GIL ──→ Python code Thread 4 ─┘传统 GIL 模式下它们需要竞争解释器执行权。结果很可能是不是 4 个核心同时高效执行 Python 循环 而是多个线程轮流执行甚至因为线程调度 上下文切换 锁竞争运行时间可能比单线程还差。可以自己测试importtimefromconcurrent.futuresimportThreadPoolExecutordefcpu_work(n):total0foriinrange(n):totali*ireturntotal N8_000_000starttime.perf_counter()for_inrange(4):cpu_work(N)print(单线程:,time.perf_counter()-start)starttime.perf_counter()withThreadPoolExecutor(max_workers4)asexecutor:list(executor.map(cpu_work,[N]*4))print(4线程:,time.perf_counter()-start)具体结果会受到Python 版本 CPU 操作系统 后台负载影响所以不要把某一次测试数字当成绝对规律。但对于传统 GIL 模式下的纯 Python CPU 密集循环线程通常不会带来期望中的多核线性加速。九、CPU 密集任务怎么办用多进程传统 CPython 中一个经典方案是ProcessPoolExecutor例如fromconcurrent.futuresimportProcessPoolExecutordefcpu_work(n):total0foriinrange(n):totali*ireturntotalif__name____main__:jobs[8_000_000,8_000_000,8_000_000,8_000_000,]withProcessPoolExecutor()asexecutor:resultslist(executor.map(cpu_work,jobs))print(results)为什么多进程可以因为Process A → Python Interpreter A → GIL A Process B → Python Interpreter B → GIL B Process C → Python Interpreter C → GIL C Process D → Python Interpreter D → GIL D它们不是共同争抢一个进程里的同一把 GIL。于是操作系统可以把不同进程真正调度到多个 CPU 核心上。代价则是进程创建成本更高 内存消耗更多 数据需要序列化 进程间通信更复杂因此工程中通常这样选择场景优先考虑HTTP/API 调用threading / asyncio数据库 I/Othreading / asyncio文件 I/Othreading / asyncio大量纯 Python 数值计算multiprocessingCPU 密集任务池ProcessPoolExecutor大量高并发网络连接asyncio混合型系统asyncio thread/process pool十、一个有趣的问题NumPy 为什么能利用多核这时很多开发者会发现一个矛盾。Python 有 GIL但NumPy SciPy PyTorch TensorFlow为什么很多计算却能跑满多个 CPU 核心原因是GIL 限制的是持有 GIL 执行 Python 相关代码的线程不代表所有本地机器代码永远只能单线程执行。许多高性能库的核心计算实际上发生在C C Fortran BLAS OpenMP CUDA里面。当 C 扩展进入一段不需要操作 Python 对象的长时间计算时可以主动释放 GIL。Python 官方 C API 文档就提到除了阻塞 I/O一些长时间执行、但不需要访问 Python 对象的本地代码也可以释放相应线程状态标准库中的zlib、hashlib就包含类似做法。(Python documentation)于是可能形成Python Thread A │ ▼ 调用 NumPy │ 释放 GIL ▼ C / BLAS ├── Core 1 ├── Core 2 ├── Core 3 └── Core 4这也解释了为什么一句“Python 有 GIL所以 Python 永远不能进行并行计算。”是错误的。十一、并发和并行一定要分清楚这是理解 GIL 的另一个关键。并发 Concurrency多个任务在同一个时间段内持续推进时间 ─────────────────────→ Task A ███ ███ Task B ███ ███ Task C ███它们未必真的在同一个物理时刻执行。并行 Parallelism多个任务在同一个时刻真正运行CPU Core 1 █████████ CPU Core 2 █████████ CPU Core 3 █████████ CPU Core 4 █████████所以传统 CPythonthreading可以很好地实现并发特别是 I/O 场景。但对于纯 Python CPU 密集代码传统 GIL 会限制线程之间真正的 CPU 并行。十二、GIL 会一直锁着不放吗不会。传统 CPython 会在适当时机让线程之间获得执行机会。还可以查看线程切换间隔importsysprint(sys.getswitchinterval())以及设置sys.setswitchinterval(0.01)但是请不要把这个参数当成普通程序的“多线程性能调节旋钮”。绝大多数应用没有必要修改它。此外遇到blocking I/O或者某些主动释放 GIL 的 C 扩展代码时GIL 也可能被释放。所以实际运行更接近Thread A 获取 GIL ↓ 执行 Python ↓ 释放 / 被切换 Thread B 获取 GIL ↓ 执行 Python ↓ 释放 / 被切换而不是某一个线程从程序开始一直锁到程序结束。十三、Python 正在发生巨大变化Free-Threaded Python如果几年前写 GIL 教程到这里差不多可以结束了。但今天已经不能这样写。Python 的多线程模型正在经历一次非常重要的改变。Python 3.13可选 free-threaded buildPEP 703 提出了让 CPython 的 GIL 变成可选机制。Python 3.13 开始提供 free-threaded 构建使 CPython 可以在关闭 GIL 的模式下运行让多个线程真正并行执行 Python 代码。(Python Enhancement Proposals (PEPs))可以理解成传统模型Thread A ─┐ Thread B ─┼── GIL ─→ Python Thread C ─┘Free-threaded 模型Thread A ──→ Core 1 Thread B ──→ Core 2 Thread C ──→ Core 3 Thread D ──→ Core 4当然实现它远不是“把 GIL 删除”这么简单。PEP 703 涉及大量 CPython 内部改造包括引用计数 内存管理 容器线程安全 锁与原子操作否则直接把原来的 GIL 删除解释器本身就可能失去线程安全。(Python Enhancement Proposals (PEPs))十四、Python 3.14Free-Threaded 从实验走向正式支持这里尤其值得关注。Python 3.13 中 free-threaded CPython 仍处于实验阶段而到了 Python 3.14PEP 779 推动它进入Phase IIfree-threaded Python 已正式成为受支持的构建方式但仍然是可选模式而不是默认模式。(Python Enhancement Proposals (PEPs))因此截至当前 Python 3.14 系列我们不能简单说Python 已经完全没有 GIL也不能说Python 永远都会有 GIL更准确的描述是传统 GIL 构建仍然存在并广泛使用同时 CPython 已正式支持可选的 free-threaded 构建。这也是学习 Python 并发时必须更新的知识。十五、如何判断当前 Python 是否启用了 GIL在支持相关功能的 CPython 中可以检查importsysprint(sys._is_gil_enabled())可能得到True说明当前运行时启用了 GIL。如果使用 free-threaded Python 并实际关闭了 GIL则可能得到False也可以查看当前构建importsysconfigprint(sysconfig.get_config_var(Py_GIL_DISABLED))官方 free-threading 文档建议使用Py_GIL_DISABLED对构建配置进行判断而sys._is_gil_enabled()可以查看当前运行进程中 GIL 是否实际启用。(Python documentation)十六、没有 GIL是不是以后就不用 Lock 了恰恰相反。这是 free-threaded Python 时代最需要建立的新认识之一。假设inventory1两个线程ifinventory0:inventory-1即使 Python 完全没有 GIL也不意味着检查库存 修改库存突然变成业务事务。反而由于真正的多线程并行增加这类共享状态问题会更加值得重视。Free-threaded CPython 为dict、list、set等内建类型加入了内部同步机制以维持类似传统 GIL 构建的底层安全行为但官方仍明确建议应用程序应使用threading.Lock等同步工具而不是依赖内建类型内部锁作为自己的业务同步方案。(Python documentation)因此未来正确的开发思维应该是解释器线程安全 ≠ 应用程序线程安全这一点无论有没有 GIL 都不会改变。十七、Free-Threaded 为什么这么难因为过去几十年很多 CPython 内部代码和 C 扩展事实上默认同一时刻只有一个线程操作 Python 对象GIL 在某种程度上承担了一层“隐式保护”。一旦取消这把大锁就必须重新处理引用计数竞争 容器并发修改 内存分配 全局缓存 扩展模块内部状态 对象生命周期 垃圾回收例如 C 扩展过去有global_cache可能从来没有加过锁。为什么因为过去默认调用相关 Python C API 时有 GIL。到了 free-threaded 环境这个假设可能不再成立。Python 官方针对 C 扩展的迁移文档也专门提醒原来由 GIL 隐式保护的扩展内部缓存和全局状态现在可能需要自己增加锁或改用线程局部存储。(Python documentation)这句话其实非常准确地回答了本文标题GIL 到底保护了什么它长期以来不仅限制并行也实际上替大量 CPython 内部代码和扩展代码承担了一部分全局同步责任。十八、实战中到底该怎样选择并发方案理解 GIL 最终不是为了背面试题而是为了做架构决策。我更推荐从任务性质出发。场景一大量 HTTP 请求例如爬虫 API 聚合 批量下载 第三方接口优先asyncio ThreadPoolExecutor场景二数据库、文件等阻塞 I/O可以考虑threading 异步驱动 asyncio.to_thread()场景三纯 Python CPU 密集计算传统 GIL 构建下优先ProcessPoolExecutor multiprocessing例如fromconcurrent.futuresimportProcessPoolExecutordefcalculate(data):returnsum(value*valueforvalueindata)if__name____main__:datasets[range(1_000_000),range(1_000_000),range(1_000_000),range(1_000_000),]withProcessPoolExecutor()aspool:resultslist(pool.map(calculate,datasets))print(results)场景四NumPy / PyTorch 等计算先检查库本身是否已经释放 GIL 使用 BLAS 使用 OpenMP 使用 GPU不要为了“多核”再盲目套一层 Python 线程。场景五准备拥抱 Free-Threaded Python重点检查共享可变状态 第三方 C 扩展兼容性 隐式原子性假设 全局缓存 线程局部变量 业务临界区 性能基准free-threaded 不是把 Python 换个解释器然后线程数改成 32。而是重新认真审视你的并发模型。十九、关于 GIL记住这 8 句话就够了如果整篇文章只留下几个结论我希望是这些GIL 是 CPython 的解释器级同步机制不是 Python 语言本身的业务锁。传统 GIL 构建中同一时刻通常只有一个线程执行相关 Python 代码。GIL 的核心历史作用之一是帮助保护 CPython 对象、引用计数以及解释器内部状态的一致性。GIL 不保证你的业务逻辑线程安全。有 GIL 仍然需要Lock、RLock、Queue 等同步机制。I/O 密集任务依然非常适合线程因为阻塞 I/O 可以释放 GIL。传统 GIL 模式下纯 Python CPU 密集代码通常应考虑多进程而不是盲目增加线程。从 Python 3.13 开始 CPython 提供 free-threaded 构建Python 3.14 已将其推进到正式支持但仍可选的阶段。二十、结语理解 GIL比讨厌 GIL 更重要学习 Python 一段时间后我们很容易把 GIL 当成一个“限制 Python 性能的坏东西”。但如果真正深入 CPython就会发现事情远没有这么简单。GIL 给 Python 带来了限制纯 Python 多线程 CPU 并行受限但在很长时间里它也降低了解释器实现复杂度 C 扩展同步复杂度 对象管理成本如今随着 PEP 703 和 free-threaded Python 的推进CPython 正在尝试跨过这条历史边界。这意味着未来的 Python 编程可能拥有更强的原生多线程并行能力但开发者同时也必须更认真地面对Race Condition Deadlock Lock Contention Shared Mutable State Thread Safety换句话说GIL 的逐步可选化不是“并发问题消失了”而是 Python 开发者终于能够获得更强的并行能力同时也需要承担更多真正的并发设计责任。如果你是初学者不必因为 GIL 而害怕 Python 多线程。先学会判断这是 CPU Bound 还是 I/O Bound如果你已经是资深 Python 开发者那么现在则是一个非常值得关注的时间节点Python 多线程的传统经验正在发生变化。过去我们经常问“怎样绕过 GIL”未来越来越值得问的问题可能是“当 GIL 不再替我承担那部分隐式同步责任以后我的代码真的线程安全吗”而这可能才是理解 GIL 最有价值的地方。SEO 关键词Python编程、Python教程、Python实战、Python最佳实践、Python GIL、Python多线程、Python并发编程、threading、multiprocessing、CPU密集型、I/O密集型、Free-Threaded Python、PEP 703、Python 3.14。推荐延伸阅读建议继续阅读以下官方资料Pythonthreading官方文档Python C APIThread State 与 Global Interpreter LockPEP 703Making the Global Interpreter Lock Optional in CPythonPEP 779Free-Threaded Python Supported StatusPython Free-Threading 使用指南Pythonmultiprocessing与concurrent.futures文档。同时推荐进一步阅读《流畅的 Python》《Effective Python》以及《Python 编程从入门到实践》逐步把线程、协程、多进程和 Python 对象模型联系起来理解。互动讨论你在项目中是否遇到过这种情况明明用了 Python 多线程CPU 利用率却始终上不去或者因为误以为“有 GIL 就线程安全”最终遇到了共享数据竞态问题面对已经进入正式支持阶段的 free-threaded Python你会愿意在生产项目中尝试它吗欢迎分享你的实践经验。很多并发问题没有一个放之四海而皆准的答案而真正有价值的 Python 实战经验往往就藏在这些真实踩坑与取舍之中。