2.1. GIL 深度解析

全局解释器锁(Global Interpreter Lock,GIL)是 CPython 中最受争议的特性之一。理解它对于写出高效的 Python 程序至关重要。

2.1.1. 什么是 GIL

GIL 是 CPython 解释器中的一个互斥锁,它确保同一时刻只有一个线程执行 Python 字节码。

import threading
import time

counter = 0

def increment():
    global counter
    for _ in range(1000000):
        counter += 1  # 即使有 GIL,这也不是线程安全的!

# 创建多个线程
threads = [threading.Thread(target=increment) for _ in range(5)]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Expected: 5000000, Got: {counter}")
# 可能输出: Expected: 5000000, Got: 3847291

警告

常见误解:GIL 不保证线程安全!

counter += 1 在字节码层面是多个操作:

  1. 读取 counter 值

  2. 加 1

  3. 写回 counter

GIL 可能在任何操作之间释放。

2.1.2. GIL 的工作原理

        sequenceDiagram
    participant T1 as Thread 1
    participant GIL as GIL
    participant T2 as Thread 2
    
    T1->>GIL: 获取 GIL
    Note over T1: 执行 Python 代码
    T1->>GIL: 释放 GIL (I/O 或超时)
    T2->>GIL: 获取 GIL
    Note over T2: 执行 Python 代码
    T2->>GIL: 释放 GIL
    T1->>GIL: 获取 GIL
    Note over T1: 继续执行
    

2.1.2.1. GIL 释放时机

# 1. I/O 操作时自动释放
def io_operation():
    with open('file.txt') as f:
        data = f.read()  # GIL 释放
    return data

# 2. 调用 C 扩展时可能释放
import numpy as np
arr = np.array([1, 2, 3])
result = np.sum(arr)  # NumPy 操作中 GIL 释放

# 3. 使用 sleep 时释放
import time
time.sleep(1)  # GIL 释放

# 4. 使用 sys.setswitchinterval 控制切换间隔
import sys
print(sys.getswitchinterval())  # 默认 0.005 秒
# sys.setswitchinterval(0.001)  # 可以调整

2.1.3. GIL 的影响

2.1.3.1. CPU 密集型任务

import threading
import time

def cpu_intensive(n):
    """CPU 密集型任务"""
    total = 0
    for i in range(n):
        total += i * i
    return total

# 单线程
start = time.perf_counter()
for _ in range(4):
    cpu_intensive(5_000_000)
single_thread_time = time.perf_counter() - start
print(f"Single thread: {single_thread_time:.2f}s")

# 多线程
start = time.perf_counter()
threads = [
    threading.Thread(target=cpu_intensive, args=(5_000_000,))
    for _ in range(4)
]
for t in threads:
    t.start()
for t in threads:
    t.join()
multi_thread_time = time.perf_counter() - start
print(f"Multi thread: {multi_thread_time:.2f}s")

# 结果:多线程可能更慢!因为线程切换开销

2.1.3.2. I/O 密集型任务

import threading
import time
import urllib.request

def fetch_url(url):
    """I/O 密集型任务"""
    with urllib.request.urlopen(url) as response:
        return len(response.read())

urls = ['https://www.python.org'] * 10

# 单线程
start = time.perf_counter()
for url in urls:
    fetch_url(url)
single_time = time.perf_counter() - start
print(f"Single thread: {single_time:.2f}s")

# 多线程
start = time.perf_counter()
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
    t.start()
for t in threads:
    t.join()
multi_time = time.perf_counter() - start
print(f"Multi thread: {multi_time:.2f}s")

# 结果:多线程显著更快

2.1.4. 绕过 GIL 的方法

2.1.4.1. 1. 多进程

from multiprocessing import Pool
import time

def cpu_intensive(n):
    total = 0
    for i in range(n):
        total += i * i
    return total

if __name__ == '__main__':
    # 使用进程池
    start = time.perf_counter()
    with Pool(4) as pool:
        results = pool.map(cpu_intensive, [5_000_000] * 4)
    print(f"Multi process: {time.perf_counter() - start:.2f}s")
    # 真正的并行,接近 4 倍加速

2.1.4.2. 2. 使用释放 GIL 的库

import numpy as np
import time

# NumPy 在执行计算时释放 GIL
arr = np.random.rand(10000000)

start = time.perf_counter()
result = np.sum(arr ** 2)  # GIL 在这里被释放
print(f"NumPy: {time.perf_counter() - start:.4f}s")

# 纯 Python 对比
lst = list(arr)
start = time.perf_counter()
result = sum(x ** 2 for x in lst)
print(f"Pure Python: {time.perf_counter() - start:.4f}s")

2.1.4.3. 3. Cython 释放 GIL

# cython_example.pyx
from cython.parallel import prange

def parallel_sum(double[:] arr):
    cdef double total = 0
    cdef int i
    cdef int n = arr.shape[0]
    
    # nogil 上下文中释放 GIL
    with nogil:
        for i in prange(n):
            total += arr[i] * arr[i]
    
    return total

2.1.4.4. 4. 使用其他 Python 实现

# PyPy - 没有 GIL(STM 版本)
# Jython - 基于 JVM,没有 GIL
# IronPython - 基于 .NET,没有 GIL

# 注意:这些实现可能不支持某些 C 扩展

2.1.5. 线程安全的编程

2.1.5.1. 使用锁

import threading

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(1000000):
        with lock:  # 获取锁
            counter += 1

threads = [threading.Thread(target=safe_increment) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Expected: 5000000, Got: {counter}")
# 现在总是正确:5000000

2.1.5.2. 使用原子操作

import threading
from queue import Queue

# Queue 是线程安全的
task_queue = Queue()

def producer():
    for i in range(100):
        task_queue.put(i)

def consumer():
    while True:
        item = task_queue.get()
        if item is None:
            break
        print(f"Processing {item}")
        task_queue.task_done()

# 使用线程安全的数据结构避免显式锁

2.1.5.3. threading.local

import threading

# 线程本地存储
thread_local = threading.local()

def process_request(request_id):
    thread_local.request_id = request_id
    # 在同一线程的任何地方访问
    do_work()

def do_work():
    # 每个线程有自己的 request_id
    print(f"Working on request {thread_local.request_id}")

2.1.6. 最佳实践

GIL 不是问题的情况
  1. I/O 密集型任务:GIL 在 I/O 时释放

  2. 使用 NumPy/Pandas:底层计算释放 GIL

  3. C 扩展计算:可以释放 GIL

  4. 单线程应用:无影响

GIL 是问题的情况
  1. 纯 Python CPU 密集型:使用多进程

  2. 需要真正并行:考虑多进程或其他语言

  3. 高性能计算:使用 Cython 或 Numba

关键原则
  1. GIL 不保证线程安全:仍需要同步机制

  2. 测量后再优化:不要假设性能瓶颈

  3. 选择合适的并发模型:asyncio、threading、multiprocessing 各有适用场景

2.1.7. Python 3.12+ 的变化

Python 正在进行移除 GIL 的工作(PEP 703):

# 未来可能的 Python 构建选项
# --disable-gil

# 这将允许真正的多线程并行
# 但需要解决引用计数的线程安全问题

备注

截至 Python 3.12,这仍是实验性功能。在生产环境中,仍应按当前的 GIL 行为编程。