目錄
05-1. Python GIL 深度解析
⏱️ 閱讀時間: 15 分鐘 🎯 難度: ⭐⭐⭐ (中等)
🎯 本篇重點
深入理解 Python GIL (Global Interpreter Lock) 的原理、影響以及如何繞過它。
🤔 什麼是 GIL?
GIL (Global Interpreter Lock) = 全域解釋器鎖
一句話解釋: GIL 是 Python 解釋器(CPython)的一把全域鎖,確保同一時間只有一個 Thread 可以執行 Python 程式碼。
🏢 用廚房來比喻
沒有 GIL 的語言(如 Java、C++)
廚房(CPU)
├─ 爐子 1(核心 1)→ 廚師 A 炒菜
├─ 爐子 2(核心 2)→ 廚師 B 炒菜
├─ 爐子 3(核心 3)→ 廚師 C 炒菜
└─ 爐子 4(核心 4)→ 廚師 D 炒菜
4 個廚師可以同時使用 4 個爐子 ← 真正並行有 GIL 的 Python
廚房(CPU)
├─ 爐子 1(核心 1)
├─ 爐子 2(核心 2)
├─ 爐子 3(核心 3)
└─ 爐子 4(核心 4)
但只有一把廚房鑰匙(GIL)!
├─ 廚師 A 持有鑰匙 → 可以用爐子 1
├─ 廚師 B 等待鑰匙 → 無法工作
├─ 廚師 C 等待鑰匙 → 無法工作
└─ 廚師 D 等待鑰匙 → 無法工作
同一時間只有一個廚師可以工作 ← 無法並行🔍 GIL 的實際影響
案例 1:CPU 密集型任務
import time
from threading import Thread
def cpu_task():
"""CPU 密集計算"""
total = 0
for i in range(10000000):
total += i * i
return total
# 單 Thread
start = time.time()
cpu_task()
cpu_task()
print(f"單 Thread: {time.time() - start:.2f} 秒")
# 多 Thread
start = time.time()
t1 = Thread(target=cpu_task)
t2 = Thread(target=cpu_task)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"多 Thread: {time.time() - start:.2f} 秒")輸出(4 核 CPU):
單 Thread: 2.5 秒
多 Thread: 2.5 秒 ← 沒有加速!原因:GIL 限制,兩個 Thread 無法同時執行
案例 2:I/O 密集型任務
import time
import requests
from threading import Thread
def io_task():
"""I/O 密集任務"""
response = requests.get('https://httpbin.org/delay/1')
return response.status_code
# 單 Thread
start = time.time()
io_task()
io_task()
print(f"單 Thread: {time.time() - start:.2f} 秒")
# 多 Thread
start = time.time()
t1 = Thread(target=io_task)
t2 = Thread(target=io_task)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"多 Thread: {time.time() - start:.2f} 秒")輸出:
單 Thread: 2.1 秒
多 Thread: 1.1 秒 ← 有加速!原因:I/O 操作時會釋放 GIL,其他 Thread 可以執行
💡 為什麼要有 GIL?
原因 1:記憶體管理
Python 使用引用計數(Reference Counting)管理記憶體:
a = [] # 引用計數 = 1
b = a # 引用計數 = 2
del a # 引用計數 = 1
del b # 引用計數 = 0 → 釋放記憶體沒有 GIL 的問題:
Thread A: 增加引用計數(讀取 → +1 → 寫入)
Thread B: 增加引用計數(讀取 → +1 → 寫入)
期望:計數 +2
實際:計數 +1(Race Condition!)GIL 的解決:
- 確保同一時間只有一個 Thread 修改引用計數
- 避免 Race Condition
原因 2:簡化 C 擴展
許多 Python C 擴展不是線程安全的:
// C 擴展(非線程安全)
PyObject* my_function(PyObject* self, PyObject* args) {
// 假設這裡有非線程安全的操作
static int counter = 0;
counter++;
return PyLong_FromLong(counter);
}GIL 確保:
- C 擴展不需要自己實作線程安全
- 降低開發成本
🔄 GIL 的工作原理
GIL 釋放時機
# 1. I/O 操作時釋放 GIL
file.read() # 釋放 GIL
requests.get() # 釋放 GIL
time.sleep() # 釋放 GIL
# 2. 執行一定數量的字節碼後釋放
# CPython 3.2+: 每 15 毫秒切換一次
for i in range(10000000):
x = i * i # 定期釋放 GILGIL 切換流程
時間 0ms:
Thread A 獲得 GIL → 執行 Python 程式碼
時間 15ms:
Thread A 釋放 GIL
Thread B 獲得 GIL → 執行 Python 程式碼
時間 30ms:
Thread B 釋放 GIL
Thread A 獲得 GIL → 執行 Python 程式碼
...持續切換🎯 如何繞過 GIL?
方法 1:使用 multiprocessing
from multiprocessing import Process
import time
def cpu_task():
total = 0
for i in range(10000000):
total += i * i
# 多 Process(繞過 GIL)
start = time.time()
p1 = Process(target=cpu_task)
p2 = Process(target=cpu_task)
p1.start(); p2.start()
p1.join(); p2.join()
print(f"多 Process: {time.time() - start:.2f} 秒")輸出(4 核 CPU):
多 Process: 1.3 秒 ← 有加速!原因:
- 每個 Process 有獨立的 Python 解釋器
- 每個 Process 有自己的 GIL
- 可以真正並行
方法 2:使用 C 擴展
# NumPy 內部用 C 實作,釋放 GIL
import numpy as np
from threading import Thread
def numpy_task():
arr = np.random.rand(10000, 10000)
result = np.dot(arr, arr) # 內部釋放 GIL
# 多 Thread 可以並行(NumPy 釋放 GIL)
t1 = Thread(target=numpy_task)
t2 = Thread(target=numpy_task)
t1.start(); t2.start()
t1.join(); t2.join()方法 3:使用無 GIL 的 Python 實作
# Jython(Java 實作的 Python)
jython script.py
# IronPython(.NET 實作的 Python)
ipy script.py
# PyPy(JIT 編譯的 Python)
pypy script.py
# 注意:這些實作可能不支援所有 Python 套件方法 4:使用 asyncio
import asyncio
import aiohttp
async def io_task(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [f'https://httpbin.org/delay/1' for _ in range(10)]
tasks = [io_task(url) for url in urls]
results = await asyncio.gather(*tasks)
# 單 Thread,無 GIL 問題
asyncio.run(main())原因:
- asyncio 使用協程(Coroutine)
- 單 Thread,無需多 Thread
- 沒有 GIL 競爭問題
📊 GIL 影響總結
CPU 密集型
| 方案 | GIL 影響 | 效能 |
|---|---|---|
| threading | ❌ 嚴重 | 無加速 |
| multiprocessing | ✅ 無影響 | 有加速 |
| C 擴展 | ✅ 可釋放 | 有加速 |
I/O 密集型
| 方案 | GIL 影響 | 效能 |
|---|---|---|
| threading | ✅ 輕微 | 有加速 |
| multiprocessing | ✅ 無影響 | 有加速(成本高) |
| asyncio | ✅ 無影響 | 有加速(最佳) |
🔍 檢查 GIL 的影響
import sys
import threading
def check_gil():
print(f"Python 版本: {sys.version}")
print(f"GIL 切換間隔: {sys.getswitchinterval()} 秒")
# 修改 GIL 切換間隔
sys.setswitchinterval(0.001) # 1 毫秒
print(f"新的切換間隔: {sys.getswitchinterval()} 秒")
check_gil()✅ 重點回顧
GIL 的本質:
- Python 解釋器的全域鎖
- 確保同一時間只有一個 Thread 執行 Python 程式碼
GIL 的影響:
- ❌ CPU 密集型:多 Thread 無法並行
- ✅ I/O 密集型:I/O 時釋放 GIL,影響較小
為什麼要有 GIL:
- ✅ 簡化記憶體管理(引用計數)
- ✅ 保護 C 擴展的線程安全
- ✅ 降低開發成本
如何繞過 GIL:
- multiprocessing:多進程,獨立 GIL
- C 擴展:在 C 層面釋放 GIL
- asyncio:單 Thread 協程,無 GIL 競爭
- 其他 Python 實作:Jython, IronPython
關鍵理解:
- ✅ GIL 是 CPython 的實作細節
- ✅ 不影響 I/O 密集型應用
- ✅ CPU 密集型應該用 multiprocessing
- ✅ 未來 Python 可能移除 GIL(Python 3.13+)
最後更新:2025-01-04