你第一次在 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 的 threading 對 I/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.13t,t 表示 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 密集計算。