Python技术迷

python面试:什么是 GIL 以及绕过它的一些方法是什么?

说到 Python 面试里绕不开的“GIL”这玩意儿,说实话,面试官一旦问起来,不仅是对你理解多线程的基本功一次大考察,更是你对 Python 内核机制了解的试金石。

要我说,这 GIL,全名叫 Global Interpreter Lock,全局解释器锁,一听这名字就透着点霸道总裁那味儿:整个 Python 解释器只能同时被一个线程拥有访问权,谁抢到谁说了算,其他线程你再急也得排队。

很多兄弟姐妹第一次接触 GIL,都是在写多线程爬虫、搞异步处理的时候一脸懵逼:为啥我开的十个线程,性能反倒没上去?为啥 CPU 使用率永远满不了?我去,这不坑爹么?

GIL 到底是个什么鬼?

咱先别着急骂它坑,GIL 不是 Python 的 bug,它是 CPython 实现里的设计选择。

CPython 是 Python 最主流的实现,是用 C 写的。而 CPython 中的内存管理(比如引用计数)不是线程安全的。为了避免多个线程同时操作导致内存数据错乱(比如引用计数变成了负数),干脆整个解释器用一把大锁锁起来——这就是 GIL。

这样设计当然简单粗暴,开发效率也高,但带来的副作用也是肉眼可见的:多线程的 CPU 密集型程序在 Python 里完全就是个笑话。

你以为自己搞了个 8 核 16 线程的高性能服务器,Python 却笑眯眯告诉你:“不好意思,我一次只用一个核。”

真·坑体验:线程是摆设

讲真,写 Python 多线程代码的时候,有种“全村的希望都交给你了”的豪情,但现实往往啪啪打脸。

举个例子,我们搞个 CPU 密集的计算,比如计算一堆斐波那契数列的结果:

import threading

deffib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)

threads = []
for i in range(4):
    t = threading.Thread(target=fib, args=(35,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

看着线程数是 4,你以为自己在并行计算,结果你用 top 一看,CPU 使用率压根就没满……这就是 GIL 的魔力,每次只有一个线程能执行 Python 字节码,其他线程只能傻傻等。

那多线程就不能用了?

也不是这么绝对。GIL 主要影响的是 CPU 密集型 的程序,对于 IO 密集型 的场景,比如网络请求、文件读写、数据库操作,多线程依然能带来收益。

为啥?因为线程在等待 IO 时会自动释放 GIL,其他线程就能趁机执行。这时候多线程还是挺香的。

这也是为啥 Python 爬虫常用 threading 或 concurrent.futures.ThreadPoolExecutor 来搞定一堆请求任务,因为它们大部分时间都在等服务器响应,CPU 用得很少。

绕过 GIL 的几种姿势

说了这么多,那问题来了:GIL 这么烦,有没有办法绕过去?

当然有,而且不止一种。看情况选适合自己的方案吧:

1. 多进程:用 multiprocessing 解放 CPU

最直接的方式就是用多个进程代替线程。进程之间各自有独立的 Python 解释器和 GIL,互不干扰。

from multiprocessing import Process

deffib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)

processes = []
for _ in range(4):
    p = Process(target=fib, args=(35,))
    processes.append(p)
    p.start()

for p in processes:
    p.join()

这代码一跑,CPU 立马拉满!👍 不过多进程也不是银弹,它的开销比线程大,进程间通信(IPC)也更复杂,不适合短小轻巧的任务。

2. 用 C/C++ 写扩展模块,避开 GIL 限制

GIL 只限制执行 Python 字节码,但如果你写个 C 扩展,计算逻辑用纯 C 写,在代码里显式地释放 GIL,就可以实现真正的并行。

比如你可以用 cython,在某段计算前加上 with nogil:,告诉解释器:“这段我自己来搞,不用你管。”

cdef int fib(int n) nogil:
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

当然这门槛就高多了,属于“想要并行,先写 C”的流派。

3. 用 numpy、pandas 等底层 C 实现的库

很多 Python 科学计算库本身就封装了底层并行逻辑,比如 numpy 调用的是 BLAS 或 MKL 库,它们内部就能开线程。

所以像这种代码:

import numpy as np

a = np.random.rand(10000, 10000)
b = np.dot(a, a.T)

即使你只写了一行 Python,它在背后调用了并行优化的 C 库,性能杠杠的。

4. 上 async,大胆异步起来

Python 3.5 之后有了 asyncio,你可以不用线程或进程,就搞定高并发 IO 操作。比如爬虫、网络服务、数据采集这些场景,async/await 简直香到飞起。

但别忘了,它不能让你的计算更快,只是把等待的时间利用起来了。

-END-

我为大家打造了一份RPA教程,完全免费:https://www.songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领!