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)。計數低於實際引用數時,物件可能提早釋放而造成崩潰;計數高於實際引用數時,即使引用已經歸零,物件仍無法回收而產生記憶體洩漏。
以遞減參考計數的競爭為例:
- 計數原為
2。 - 兩條執行緒同時讀到
2。 - 各自減一後都寫回
1。 - 實際引用已經歸零,計數卻仍是
1。
在一般的參考計數回收路徑中,CPython 只會在計數降至 0 時釋放物件。此時它誤以為物件仍被使用,記憶體便無法回收而形成洩漏。
為何 GIL 是當時的合理選擇
要保護計數器,直譯器主要有兩種選擇:
- 細粒度鎖(Fine-grained Locks):為每個物件獨立配置一把互斥鎖,每次增減計數皆須上鎖與解鎖。
- 粗粒度全域鎖(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:,}")
在不同環境下實測,結果呈現明顯的行為分歧:
- Python 3.9(GIL 開啟):4 條執行緒累加 400 萬次,實際結果僅得約 140 萬至 260 萬次,遺失了過半數的加總。
- Python 3.14(GIL 開啟):此例剛好為 400 萬次,原因是 3.10 引入的 commit 4958f5d 調整了切換檢查時機,單純迴圈沒有觸發切換點。Python 語言並未保證這項結果,換成函式呼叫後仍可能出現競爭條件。
- Python 3.14t(Free-threaded,GIL 關閉):多核真正平行執行,數值僅剩約 110 萬次,遺失近 7 成資料。
這項實測顯示,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 處理的是不同層次的問題:
- GIL 限制:同一個 CPython 行程中,多條執行緒通常不能同時執行 Python 位元組碼。
- 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/1999 | Greg Stein Patch | 對直譯器內部容器與計數器加上細粒度鎖 | 單執行緒效能衰退將近 2 倍至 4 倍,未被採用 |
| 2007 | Guido 發表公開準則 | 定調移除 GIL 的前提條件 | 確立單執行緒零衰退原則 |
| 2011(3.2) | Antoine Pitrou(新 GIL) | 改進切換機制,改採固定時間間隔(5 毫秒) | 改善多執行緒競爭開銷,但並未移除 GIL 本身 |
| 2016 | Larry Hastings(Gilectomy) | 針對 CPython 3.x 進行細粒度鎖改造 | 記憶體原子操作造成單執行緒巨幅衰退,最終停擺 |
| 2017 | PEP 554 / Eric Snow | 探索 Subinterpreter(子直譯器)隔離模型 | 為每直譯器獨立 GIL 奠定基礎,走向多直譯器路線 |
| 2021 | Sam 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 組合五項技術,只有必要時才使用成本較高的同步機制,避免把所有計數器一律換成原子操作:
- 偏向參考計數(Biased Reference Counting):大部分 Python 物件僅被建立它的執行緒存取。計數器拆為「擁有者專用」與「跨執行緒共享」兩組;擁有者走極快的普通加減,僅外部執行緒存取時才走原子操作。
- 不朽物件(Immortal Objects,PEP 683):針對
None、True、小整數與內建字串,將計數器設為特殊常數,引用增減直接視為空操作(No-op),完全消除常數物件的計數競爭。 - 延遲參考計數(Deferred Reference Counting):針對函式、模組與 Code Objects 等生命週期長且頻繁存取的物件,執行期間跳過即時計數,改由垃圾回收(GC)統一掃描結算。
- mimalloc 記憶體配置器:改用執行緒安全的 mimalloc,使多執行緒配置記憶體時無需爭搶全域鎖,並讓
dict、list等內建資料結構讀取時不必加鎖。 - 物件級臨界區(Critical Sections):以細粒度臨界區保護個別容器物件;遇到阻塞呼叫時自動暫時釋放物件鎖以避免死結。
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(子直譯器,PEP 684 / PEP 734):在同一個行程內啟動多個互相隔離的直譯器實例,每個實例擁有獨立的 GIL。它兼具 Python 狀態隔離與接近執行緒的資源成本;資料主要透過複製或跨直譯器佇列傳遞,適合任務彼此獨立、希望明確限制共享狀態的架構。
- Free-Threading(無鎖直譯器,PEP 703):直譯器內完全移除 GIL,所有執行緒共享相同的記憶體定址空間。它為需要緊密共享大規模記憶體(如機器學習矩陣與圖演算法)的情境提供了真正的原生平行運算能力。
Subinterpreters 與 subprocess 有何不同
兩者都能隔離 Python 狀態,也都能避開單一 GIL 的限制;差別在於隔離邊界。Subinterpreter 是同一個 OS 行程裡的獨立 Python 執行環境,subprocess 則是另一個完整的 OS 行程。
| 面向 | Subinterpreter | subprocess |
|---|---|---|
| 執行邊界 | 與主程式位於同一個行程 | 獨立的 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。
NOTE
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 環境的團隊,建議採取以下實務作法:
- 先確認直譯器狀態:在程式中利用
sysconfig.get_config_var("Py_GIL_DISABLED")判斷執行檔是否具備 free-threading 能力,並用sys._is_gil_enabled()檢驗目前 GIL 是否實際關閉(確認未被第三方模組強制重新開啟)。 - 不要為「未來」過早推倒重構:現有的 I/O 密集應用繼續採用
asyncio或threading即可;正式環境的 CPU 密集工作在 3.14 時代首選仍是ProcessPoolExecutor。 - 利用
PYTHON_GIL進行除錯歸因:當在無鎖環境遇到不可預期的崩潰或資料異常時,設定環境變數PYTHON_GIL=1再次執行。若問題隨之消失,便可進一步查修程式內部的執行緒競爭條件。 - 在 CI 管線中納入
3.14t矩陣:安裝python3.14t執行現有單元測試,及早發現潛藏在商業邏輯中的執行緒安全問題。