怎么绕过GIL

一、怎么绕过 GIL?(必答)

方案一:multiprocessing 多进程(最常用)

每个进程有独立的 Python 解释器和 GIL,可以真正利用多核。

通俗比喻:4 个厨师抢 1 个灶台(多线程)太挤了?那就开 4 间厨房(多进程),每间各有自己的灶台,各做各的菜——真正的并行。代价是厨房之间传菜(进程间通信)比较麻烦,而且每间厨房都要占地方(内存)。

 
from multiprocessing import Pool               # 导入进程池
 
import time                                    # 导入时间模块
 
def cpu_work(n):
 
    """CPU密集任务"""
 
    total = 0                                  # 初始化累加器
 
    for i in range(n):                         # 循环 n 次纯计算
 
        total += i                             # 每次循环占用 CPU
 
    return total                               # 返回结果
 
if __name__ == "__main__":                     # 多进程必须在 main 里写,否则会无限 fork
 
    # ---- 单进程:串行执行两次,共 ~12s ----
 
    start = time.time()                        # 记录开始时间
 
    cpu_work(100_000_000)                      # 第一个任务
 
    cpu_work(100_000_000)                      # 第二个任务,串行等完
 
    print(f"单进程: {time.time() - start:.2f}s")   # ~12s
 
    # ---- 多进程:两个进程各跑一个任务,各自有独立 GIL,真并行 ----
 
    start = time.time()                        # 记录开始时间
 
    with Pool(2) as p:                         # 开 2 个进程(= 2 间厨房,各有各的灶台)
 
        p.map(cpu_work, [100_000_000, 100_000_000])  # 把两个任务分给两个进程并行执行
 
    print(f"多进程: {time.time() - start:.2f}s")     # ~6s(真并行,时间减半)
 
优点缺点
真正并行,能用多核进程间通信开销大(IPC)
代码改动小内存占用高(每个进程独立内存空间)
适合 CPU 密集型序列化/反序列化有成本

方案二:C 扩展释放 GIL

NumPy、Pandas 等库的底层 C 代码可以在计算密集部分主动释放 GIL,计算完再拿回来。

通俗比喻:厨师(Python 线程)炒菜时拿着灶台钥匙(GIL),但 NumPy 说”这块活我用专业设备(C 代码)干,不需要你盯着”,于是厨师主动把钥匙挂门上(释放 GIL),让别的厨师也能用灶台。等 NumPy 干完了,再把钥匙拿回来。

 
import numpy as np                             # 导入 NumPy
 
a = np.random.rand(10000, 10000)              # 创建 10000x10000 的随机矩阵 A
 
b = np.random.rand(10000, 10000)              # 创建 10000x10000 的随机矩阵 B
 
c = np.dot(a, b)                              # 矩阵乘法:底层 C/Fortran 执行,自动释放 GIL
 
                                               # 所以多线程调用 np.dot 时,线程可以真正并行利用多核
 

一句话讲清:像 NumPy 这样的库,底层用 C/Fortran 实现,计算时会调用 Py_BEGIN_ALLOW_THREADS 释放 GIL,计算完再 Py_END_ALLOW_THREADS 重新获取。所以用 NumPy 做矩阵运算时,多线程是可以利用多核的。

方案三:asyncio 异步 IO(IO 密集首选)

不是绕过 GIL,而是在等待 IO 时让出控制权,让其他协程执行。

通俗比喻:多线程像4 个厨师各端一盘菜等出餐,每人占一个位子(~8MB 栈内存);asyncio 像1 个厨师同时盯着 4 个锅,哪个锅好了就去处理那个,不占额外位子(协程只要几 KB)。区别是:多线程是”多人干活”,asyncio 是”一个人眼疾手快轮着干”。

 
import asyncio, aiohttp                       # 导入异步 IO 框架和异步 HTTP 库
 
async def fetch(session, url):
 
    """异步请求:遇到 await 就让出控制权,去处理别的协程"""
 
    async with session.get(url) as resp:       # 发起异步 GET 请求,等待时不阻塞线程
 
        return await resp.text()               # 等响应时,别的协程可以跑
 
async def main():
 
    async with aiohttp.ClientSession() as session:  # 创建异步 HTTP 会话
 
        # 创建 4 个协程任务,同时发请求
 
        tasks = [fetch(session, "https://httpbin.org/delay/1") for _ in range(4)]
 
        await asyncio.gather(*tasks)           # 并发执行所有任务,全部完成才返回
 
asyncio.run(main())                            # ~1s(和多线程一样快,但只用了 1 个线程 + 几 KB 内存)
 
对比多线程asyncio
并发模型操作系统线程切换单线程协程切换
内存开销每个线程 ~8MB 栈每个协程 ~几KB
适合场景IO密集 + 需要用同步库IO密集 + 纯异步生态
编程难度简单,但要注意线程安全需要 async/await 全链路

方案四:换解释器(了解即可)

解释器GIL 情况说明
PyPy有 GIL,但 JIT 编译后快很多CPU 密集型场景用 PyPy 可能比 CPython 快 10 倍
Jython无 GIL基于 JVM,但生态兼容性差
IronPython无 GIL基于 .NET,用的人少
CPython 3.13+可选关闭 (-X nogil)实验性,2024 年推出,未来可能成为默认

速记卡(面试闪卡)

Q1:一句话讲清「怎么绕过GIL」到底是什么?

A:绕过 GIL 只有两条真并行路:多进程与 C 扩展释放 GIL;asyncio 只是 IO 时让权不并行。

Q2:一、multiprocessing 多进程(真并行) —— 怎么理解?

A:每个进程有独立的 Python 解释器和 GIL,可真正利用多核。比喻:4 个厨师抢 1 个灶台太挤?开 4 间厨房,每间各有自己的灶台各做各的——真并行。代价是进程间通信麻烦、内存占用高。英文:multiprocessing(多进程)、IPC(进程间通信)。

Q3:二、C 扩展释放 GIL(NumPy/Pandas) —— 怎么理解?

A:NumPy/Pandas 底层 C 代码在计算密集部分主动释放 GIL,算完再拿回。比喻:厨师炒菜拿着灶台钥匙(GIL),NumPy 说”这活用专业设备干”——主动把钥匙挂门上,让别人也能用灶台。所以多线程调 np.dot 能真并行。英文:Py_BEGIN_ALLOW_THREADS、C extension(C 扩展)。

Q4:三、asyncio 异步 IO(不是绕过 GIL) —— 怎么理解?

A:asyncio 不是绕过 GIL,而是等待 IO 时让出控制权给别的协程。比喻:多线程像 4 个厨师各端一盘菜等出餐(每人占 ~8MB 栈);asyncio 像 1 个厨师盯 4 个锅,哪个好了处理哪个(几 KB)。区别:多线程多人干活,asyncio 一人轮着干。英文:asyncio、coroutine(协程)、await。

Q5:四、换解释器与选型总结 —— 怎么理解?

A:PyPy 有 GIL 但 JIT 快很多;Jython/IronPython 无 GIL 但生态差;CPython 3.13+ 可 -X nogil 实验性关闭。选型:CPU 密集→多进程或 C 扩展;IO 密集→asyncio 或多线程;记住 asyncio 不并行只是让权。英文:PyPy、nogil、interpreter(解释器)。

Q6:核心速记主线有哪些?

  • 真并行只有两条路:多进程、C 扩展释放 GIL

  • 多进程每进程独立 GIL,代价是 IPC 与内存

  • C 扩展(NumPy)计算时释放 GIL

  • asyncio 只是 IO 时让权,不并行

  • CPU 密集用多进程/C 扩展,IO 密集用 asyncio

口诀

A:GIL 锁住单线程,多进程各开一间房;

C 扩展算时松手,NumPy 并行不慌张。

asyncio 盯锅转,让权不并行为哪桩;

CPU 密集多进程,IO 密集它最强。

相关链接