Python GIL 30 年演進史

在 Python 三十多年的歷史中,全域直譯器鎖(Global Interpreter Lock,GIL) 始終是社群最常爭論的核心設計。它讓直譯器實作保持簡潔、單執行緒執行極快,卻也讓需要 CPU 運算的多執行緒程式長年被困在單一核心之內。

自 1992 年引入以來,社群多次嘗試移除 GIL,但都因無法承受的單執行緒效能衰退而告終。直到 PEP 703 提出新的記憶體架構,Python 才在 3.13 推出實驗性的 free-threaded build,並在 Python 3.14 正式列入官方支援。

移除 GIL 會牽動 CPython 的記憶體管理底層,也會改變 Python 的並行模式與 C 擴充生態系。


1992 年的起點:一把鎖換來三十年的繁榮

要理解 GIL,必須回到 CPython 的記憶體管理核心——參考計數(Reference Counting)。在 CPython 中,每個物件都維護一個計數器,記錄目前有多少地方正在引用它;計數歸零時,物件便立即釋放記憶體。

 +----------------------------------------------------+
 |                  CPython Process                   |
 |                                                    |
 |  Thread 1 (Running)       Thread 2 (Waiting)       |
 |  +--------------------+    +--------------------+  |
 |  | Holds the GIL      |    | Blocked on GIL     |  |
 |  | Executes Bytecode  |    | Cannot Execute     |  |
 |  +--------------------+    +--------------------+  |
 |           |                                        |
 |           v                                        |
 |  +----------------------------------------------+  |
 |  |            Shared Object Memory              |  |
 |  | [Object A: refcount]   [Object B: refcount]  |  |
 |  +----------------------------------------------+  |
 +----------------------------------------------------+

在多執行緒環境下,若兩條執行緒同時修改同一個物件的計數器,極可能引發競爭條件(Race Condition)。計數低於實際引用數時,物件可能提早釋放而造成崩潰;計數高於實際引用數時,即使引用已經歸零,物件仍無法回收而產生記憶體洩漏。

以遞減參考計數的競爭為例:

在一般的參考計數回收路徑中,CPython 只會在計數降至 0 時釋放物件。此時它誤以為物件仍被使用,記憶體便無法回收而形成洩漏。

為何 GIL 是當時的合理選擇

要保護計數器,直譯器主要有兩種選擇:

  1. 細粒度鎖(Fine-grained Locks):為每個物件獨立配置一把互斥鎖,每次增減計數皆須上鎖與解鎖。
  2. 粗粒度全域鎖(Coarse-grained GIL):為整個直譯器設置單一互斥鎖,同一時間只允許單一執行緒執行位元組碼。

1992 年 8 月 4 日,Guido van Rossum 在 commit 1984f1e 中引入 threadmodule.c,並在 ceval.c 中加入了 interpreter_lock。在單核為主流的時代,全域鎖只帶來很低的執行成本,同時大幅降低了撰寫 C 擴充模組的門檻。

這項設計降低了 NumPy 等 C 擴充套件的開發門檻;多核處理器普及後,它也成了 CPU 密集多執行緒程式的限制。


迷思澄清:GIL 保護的是直譯器,而非你的程式

許多開發者常有一種誤解,以為「有了 GIL,多執行緒程式就自然具備執行緒安全」。事實上,GIL 僅保護 CPython 直譯器自身的內部狀態與記憶體一致性,完全不保護使用者的商業邏輯。

以常見的 counter += 1 為例,在底層實際上拆解為「讀取數值」、「進行加法」、「寫回變數」三個獨立步驟。只要執行緒在步驟之間發生切換,共享資料的更新就會被覆蓋。

import sys
import threading

counter = 0

def work(n):
    global counter
    for _ in range(n):
        counter += 1  # 讀取 -> 加一 -> 寫回,非原子操作

N, THREADS = 1_000_000, 4
threads = [threading.Thread(target=work, args=(N,)) for _ in range(THREADS)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"預期: {N * THREADS:,}, 實際: {counter:,}")

在不同環境下實測,結果呈現明顯的行為分歧:

這項實測顯示,GIL 不等於執行緒安全:無論 GIL 是否存在,多執行緒共享的可變狀態都必須依賴 threading.Lock 或 queue.Queue 進行同步。

為何 GIL 不太影響 I/O 工作

GIL 對 I/O 密集工作的影響通常不大。這類工作多半在等待網路、磁碟或資料庫,而不是持續執行 Python 程式碼。當一條執行緒進入 I/O 等待或 time.sleep() 時,直譯器會釋放 GIL,讓另一條執行緒接手工作。多條執行緒雖然不能同時執行 Python 位元組碼,卻能把彼此的等待時間重疊起來。

純 Python 的 CPU 密集工作正好相反:大部分時間都在執行位元組碼,各執行緒只能輪流持有 GIL,因此無法藉由多執行緒利用多核心。部分 C 擴充模組則是例外,因為它們會在計算時主動釋放 GIL。

在 Apple M4 Max(16 核心)、macOS 與 Python 3.14.7 上的微型測試正好反映這項差異:4 個各等待 0.5 秒的 I/O 工作,改用 4 條執行緒後,耗時從 2.0 秒縮減至 0.51 秒。純 CPU 密集計算在 GIL 開啟時幾乎毫無加速效果(0.33 秒 vs 0.32 秒);關閉 GIL 後,耗時從 0.26 秒降至 0.06 秒。

async 解決的不是 GIL

GIL 與 async 處理的是不同層次的問題:

傳統多執行緒本來就能在等待 I/O 時釋放 GIL。async 的價值在於避免為每項任務建立一條執行緒;asyncio 由事件迴圈在協程(Coroutine)之間切換,通常具有以下優勢:

即使未來 Python 完全移除 GIL,async 仍然有價值。它無法解決 CPU 密集計算;若直接在事件迴圈中執行大量計算,反而會阻塞其他協程。這類工作仍應交給多行程、會釋放 GIL 的 C 擴充模組,或 free-threaded Python 處理。

NOTE

延伸閱讀:Python 非同步 20 年演進史


歷代無鎖化嘗試的失敗代價

自 1990 年代起,社群多次嘗試移除 GIL,但都受挫於嚴苛的效能代價:

「我只接受在不降低單執行緒程式(以及多執行緒但 I/O 密集程式)效能的前提下,移除 GIL 的方案。」—— Guido van Rossum(2007)

年份/版本嘗試專案與推動者架構方案結果與效能衝擊
1996/1999Greg Stein Patch對直譯器內部容器與計數器加上細粒度鎖單執行緒效能衰退將近 2 倍至 4 倍,未被採用
2007Guido 發表公開準則定調移除 GIL 的前提條件確立單執行緒零衰退原則
2011(3.2)Antoine Pitrou(新 GIL)改進切換機制,改採固定時間間隔(5 毫秒)改善多執行緒競爭開銷,但並未移除 GIL 本身
2016Larry Hastings(Gilectomy)針對 CPython 3.x 進行細粒度鎖改造記憶體原子操作造成單執行緒巨幅衰退,最終停擺
2017PEP 554 / Eric Snow探索 Subinterpreter(子直譯器)隔離模型為每直譯器獨立 GIL 奠定基礎,走向多直譯器路線
2021Sam Gross(nogil)偏向參考計數、不朽物件與 mimalloc 整合成功將單執行緒損失壓低至 5%–6%,成為 PEP 703 原型

移除 GIL 為何這麼昂貴

歷代嘗試的共同瓶頸,是原子操作(Atomic Operations)與快取行彈跳(Cache-line Bouncing) 的硬體成本。

在 Python 裡,甚至最微小的變數傳遞與屬性存取,都會頻繁更動計數器。移除全域鎖後,若每次計數加減都改用跨 CPU 核心同步的原子指令,各核心的快取便會頻繁失效,導致單執行緒效能大幅下降。


PEP 703 的五項技術

2021 年,任職於 Meta 的 Sam Gross 提出 nogil 分支;2023 年 Meta 承諾投入 3 個工程師年協助實作。PEP 703 組合五項技術,只有必要時才使用成本較高的同步機制,避免把所有計數器一律換成原子操作:

 PEP 703 五項技術的縱深防禦

 +------------------------------------------------------+
 | 1. Biased Reference Counting                         |
 |    Local Thread -> Plain add/sub (Fast Path)         |
 |    Shared Thread -> Atomic add/sub (Slow Path)       |
 +------------------------------------------------------+
 | 2. Immortal Objects (PEP 683)                        |
 |    None/True/Small Ints -> Refcount Never Changes    |
 +------------------------------------------------------+
 | 3. Deferred Reference Counting                       |
 |    Functions/Modules/Code -> Counted during GC       |
 +------------------------------------------------------+
 | 4. mimalloc Allocator + Per-Object Critical Sections |
 |    Thread-Safe Alloc + Fine-Grained Object Locks     |
 +------------------------------------------------------+

Python 3.14 官方文件記錄的 pyperformance 平均效能損失,在 macOS aarch64 約為 1%,x86-64 Linux 約為 8%。


Free-Threading 與 Subinterpreters 兩條路線

除了直接移除 GIL,CPython 同時推進了另一條架構路線:Subinterpreters。

Subinterpreters 與 subprocess 有何不同

兩者都能隔離 Python 狀態,也都能避開單一 GIL 的限制;差別在於隔離邊界。Subinterpreter 是同一個 OS 行程裡的獨立 Python 執行環境,subprocess 則是另一個完整的 OS 行程。

面向Subinterpretersubprocess
執行邊界與主程式位於同一個行程獨立的 OS 行程
啟動與記憶體成本通常較低通常較高
資源隔離Python 狀態分離,但仍共享部分行程資源各自行程擁有獨立定址空間,資源須明確繼承或傳遞
故障影響底層錯誤可能拖垮整個行程子行程崩潰通常不影響親代行程
套件相容性部分 C 擴充模組尚未支援通常較成熟

Subinterpreter 只隔離 Python 狀態,不能作為安全邊界,也不能直接共享任意 Python 物件。若目的是平行執行 Python 函式,更貼切的比較是 InterpreterPoolExecutor 與 ProcessPoolExecutor:前者以執行緒承載多個直譯器,後者使用多個行程。

CAUTION

Subinterpreter 不是安全沙箱:它只隔離 Python 物件狀態,行程層資源仍然共享,底層錯誤也可能拖垮整個行程;不可將不可信程式碼放進子直譯器,誤以為已經隔離。


Free-Threading 的三階段路線

指導委員會(Steering Council)分三個階段推進 free-threading:

 Free-Threading 三階段路線

 [ Phase I:Python 3.13 ]
 - 實驗版本 python3.13t
 - 平均額外負擔約 40%

 [ Phase II:Python 3.14(現況) ]
 - 正式支援 python3.14t
 - 單執行緒額外負擔降至 5-10%
 - 預設下載仍保留 GIL

 [ Phase II 延伸:Python 3.15(2026-10) ]
 - 引入 abi3t 穩定 ABI,C 擴充一次編譯即相容

 [ Phase III:未定時程 ]
 - free-threading 成為預設建置
 - 取決於生態系成熟度與社群投票

Phase I:Python 3.13 實驗版本

Python 3.13 首次提供 python3.13t 實驗版本;當時尚未啟用 specializing adaptive interpreter,pyperformance 的平均額外負擔約為 40%。

Phase II:Python 3.14 正式支援

目前 Python 3.14 處於 Phase II(正式支援階段)。官方安裝程式提供 python3.14t 二進位執行檔供使用者選用,但預設下載的 Python 依然保留 GIL。

Phase II 延伸:Python 3.15 引入 ABI3T

C 擴充模組的相容性是無鎖化最大的挑戰。若模組未明確宣告支援 free-threading,直譯器在載入時會跳出警告並自動重新啟用 GIL。

WARNING

服務可能看似跑在 free-threaded 建置上,實際仍在 GIL 模式:C 擴充模組未宣告支援時,載入會觸發警告並自動重新啟用 GIL,多核平行不會生效。可用 sys._is_gil_enabled() 確認 GIL 是否真的關閉。

即將於 2026 年 10 月發布的 Python 3.15 將正式引入 abi3t 穩定 ABI(C 擴充模組與直譯器之間的二進位介面,PEP 803),允許 C 擴充模組只需編譯一次 wheel(標籤為 abi3.abi3t),即可同時相容於標準版與無鎖版的 Python 執行環境。

Phase III:是否成為預設版本

「將 free-threading 轉為預設或唯一建置的 Phase III 決定尚未拍板,這取決於 CPython 內部與社群生態系的諸多因素。」—— Python 指導委員會(Steering Council)

官方甚至保留了「若轉型對生態系造成過大破壞,隨時可能終止或撤回」的條款。社群預期將持續觀察第三方套件的相容支援進度,再決定是否全面淘汰 GIL。


Python 平行方案怎麼選?

選擇方案時,先看工作是在等待 I/O、消耗 CPU,還是需要共享大量記憶體:

平行方案最適合的情境主要限制與代價最低支援版本
threading(有 GIL)I/O 密集型任務(API 呼叫、資料庫查詢、檔案存取)純 Python 計算無法平行;共享狀態仍需自行同步鎖任何 3.x 版本
asyncio超高並行(Concurrency)的 I/O 服務(Web 伺服器、微服務閘道)需依賴全非同步生態系;CPU 計算會阻塞事件迴圈Python 3.4+
multiprocessing現階段生產環境穩定的 CPU 密集計算行程啟動與 Pickle 序列化開銷大;記憶體無法共享Python 2.6+
NumPy 等 C 擴充矩陣運算、影像處理、密碼學計算僅限底層 C 實作運算釋放 GIL;物件陣列無效取決於套件版本
Subinterpreters需平行運算且希望明確限制共享狀態的工作管線跨直譯器資料需複製;部分第三方 C 擴充尚未支援Python 3.14(正式 Python API)
Free-Threading需頻繁共享記憶體之大規模 CPU 密集多執行緒計算單執行緒微幅減速;依賴的 C 模組必須支援Python 3.14(正式支援)

開發者實務遷移指引

針對打算嘗試或準備遷移至 Free-Threaded 環境的團隊,建議採取以下實務作法:

  1. 先確認直譯器狀態:在程式中利用 sysconfig.get_config_var("Py_GIL_DISABLED") 判斷執行檔是否具備 free-threading 能力,並用 sys._is_gil_enabled() 檢驗目前 GIL 是否實際關閉(確認未被第三方模組強制重新開啟)。
  2. 不要為「未來」過早推倒重構:現有的 I/O 密集應用繼續採用 asyncio 或 threading 即可;正式環境的 CPU 密集工作在 3.14 時代首選仍是 ProcessPoolExecutor。
  3. 利用 PYTHON_GIL 進行除錯歸因:當在無鎖環境遇到不可預期的崩潰或資料異常時,設定環境變數 PYTHON_GIL=1 再次執行。若問題隨之消失,便可進一步查修程式內部的執行緒競爭條件。
  4. 在 CI 管線中納入 3.14t 矩陣:安裝 python3.14t 執行現有單元測試,及早發現潛藏在商業邏輯中的執行緒安全問題。