你第一次在 Python 裡用 threading 想加速 CPU 密集任務,然後發現速度沒有提升——有時候甚至更慢。

你去查,發現 GIL(Global Interpreter Lock)。然後你可能看到很多人說「Python 的 GIL 是個垃圾設計」。

這不是全貌。


GIL 是什麼

GIL 是 CPython(Python 的標準實作)裡的一把全局鎖。它的規則是:

同一時間,只有一個 OS thread 能執行 Python bytecode。

不管你的機器有幾個 CPU 核心,Python 程式同一時間只有一個 thread 在跑 Python 程式碼。

import threading
 
def count_to_million():
    n = 0
    for _ in range(1_000_000):
        n += 1
 
# 兩個 thread
t1 = threading.Thread(target=count_to_million)
t2 = threading.Thread(target=count_to_million)
t1.start(); t2.start()
t1.join(); t2.join()
# 執行時間幾乎和單個 thread 一樣——GIL 讓它們交替執行,不是真正平行

為什麼 Guido 在 1991 年加了 GIL

Python 的記憶體管理用的是 reference counting(每個物件記著它被引用了幾次,引用數到 0 就回收)。

如果多個 thread 同時修改一個物件的 reference count,就有 race condition:

Thread 1: 讀 ref_count = 2
Thread 2: 讀 ref_count = 2
Thread 1: 寫 ref_count = 1  (減一)
Thread 2: 寫 ref_count = 1  (也減一,但正確值應該是 0)
→ 物件永遠不被回收,記憶體洩漏

加鎖在每次 reference count 操作上,能解決這個問題,但那要幾百萬個鎖,效能更慘。

GIL 的方案:一把大鎖鎖住所有 Python 執行,簡單有效,reference counting 自動安全了。

1991 年,多核 CPU 還是稀有品(大多數機器是單核),這個取捨非常合理——你不會損失真正的多核平行,換來的是記憶體管理的正確性和實作的簡單性。


GIL 在 I/O 操作時會釋放

GIL 不是一直鎖著的。當 Python thread 在做 I/O(讀檔案、等 socket、等 subprocess),GIL 會釋放,讓其他 thread 有機會跑。

import threading, requests
 
def fetch(url):
    resp = requests.get(url)  # I/O 操作,GIL 釋放
    print(resp.status_code)
 
# 多個 thread 同時 fetch,真的能並行——因為等網路時 GIL 釋放了
url = "https://example.com"
threads = [threading.Thread(target=fetch, args=(url,)) for _ in range(10)]

所以 Python 的 threadingI/O 密集任務有效,對 CPU 密集任務無效


Python 沒有 GIL 的替代方案

multiprocessing(多行程)

每個行程有自己的 Python 直譯器,沒有共享 GIL 問題,真正的多核平行。

from multiprocessing import Pool
 
def heavy_computation(n):
    return sum(i * i for i in range(n))
 
with Pool(4) as p:
    results = p.map(heavy_computation, [10**6, 10**6, 10**6, 10**6])

代價:行程間通訊(序列化/反序列化)成本高,行程啟動慢,記憶體不共享(需要複製資料)。

NumPy / C extension

NumPy 的運算在 C 層執行,C 層不受 GIL 管制。array + array 這種操作,GIL 釋放,多個 CPU 核心能真正平行。

這是為什麼資料科學用 Python 沒問題的原因——真正的計算在 NumPy/PyTorch 的 C/CUDA 層跑,GIL 不影響。


Python 3.13 的 No-GIL 實驗

2023 年,CPython 接受了 PEP 703——實驗性地讓 GIL 可選(python3.13tt 表示 free-threaded)。

這不是容易的工程:移除 GIL 需要重新設計所有地方依賴 GIL 保證的東西——reference counting、內建容器的 thread safety、C extension 的相容性。

初期結果顯示:在 CPU 密集的多 thread 場景,no-GIL 版本能實現接近線性的效能提升。代價是單 thread 效能略有退步(因為 reference counting 需要 atomic 操作),以及現有 C extension 需要更新。


為什麼 Go 和 Rust 沒有 GIL

Go 的記憶體管理是 tracing GC,不是 reference counting——GC 在一個獨立的 goroutine 跑,不需要在每次賦值時更新計數,所以不需要全局鎖。Go 的 goroutine 可以真正在多個 OS thread 上平行執行。

Rust 的記憶體管理在編譯時解決——ownership 和 borrow checker 確保了 data race 在編譯時就被發現,runtime 根本不需要鎖。

GIL 是 reference counting 和「runtime 記憶體安全」這個選擇的副產品。 選了不同的記憶體管理方式,就沒有 GIL 的問題。


實際後端開發中的 GIL 影響

你不太可能在 Python web 服務裡寫 CPU 密集邏輯——圖片處理、PDF 生成、ML 推論,這些通常放在獨立的服務(Python 的 worker、或者用 Go/Rust 的 sidecar)。

Django/FastAPI 的並行靠的是多行程或多 thread(I/O 釋放 GIL)

  • gunicorn -w 4:4 個工作行程,每個獨立的 Python 直譯器
  • 每個請求的 I/O(DB query、外部 API)都釋放 GIL,thread pool 能有效服務多個請求

GIL 是 Python 的一個顯著特性,但在標準的 web 服務架構下,它不是問題——只要你不在 request handler 裡跑純 Python 的 CPU 密集計算。

下一篇:並行資料衝突與合併策略