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  # 定期釋放 GIL

GIL 切換流程

時間 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:

  1. multiprocessing:多進程,獨立 GIL
  2. C 擴展:在 C 層面釋放 GIL
  3. asyncio:單 Thread 協程,無 GIL 競爭
  4. 其他 Python 實作:Jython, IronPython

關鍵理解:

  • ✅ GIL 是 CPython 的實作細節
  • ✅ 不影響 I/O 密集型應用
  • ✅ CPU 密集型應該用 multiprocessing
  • ✅ 未來 Python 可能移除 GIL(Python 3.13+)

下一篇: 05-2. threading 模組完整指南


最後更新:2025-01-04

0%