Python技术迷

Python 的 GIL(全局解释器锁)具体如何影响多线程性能?为什么它存在?

很多人刚学 Python 的时候一看到 threading 这个模块,都兴奋得不行,觉得可以像 Java 或 C++ 那样一堆线程一起跑,性能肯定嗖嗖地上去。结果一跑,嗯?怎么还没单线程快?这时候八成就撞上了 GIL。

GIL,全称 Global Interpreter Lock,字面意思就是在 Python 解释器里有一把全局大锁,任何线程想要执行 Python 字节码,都必须先拿到这把锁。换句话说,同一时刻,只能有一个线程在跑 Python 代码。

多线程为什么感觉像“单线程”?

举个例子,你写个多线程程序,开十个线程去算平方:

import threading

defcpu_task():
    total = 0
for i in range(10**7):
        total += i*i
return total

threads = []
for _ in range(10):
    t = threading.Thread(target=cpu_task)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

这段代码理论上十个线程同时算,应该比单线程快对吧?但实际跑下来,你会发现用时并没有减少,甚至可能更慢。原因就是每个线程都得抢那把 GIL,解释器要不停地切换,结果上下文切换开销比你算的还大。

所以在 CPU 密集型任务(比如大规模数学运算、压缩、加密这种)里,多线程基本没啥提升。

那多线程是不是就没用了?

也不能这么说。GIL 限制的是 执行字节码的并行性,但如果线程大部分时间是在等 I/O(比如读文件、爬网页、查数据库),这时候 Python 会释放 GIL,让别的线程去干活。

比如这个网络请求的例子:

import threading
import requests
import time

urls = ["https://httpbin.org/delay/2"] * 5

deffetch(url):
    resp = requests.get(url)
    print(f"done: {url}, status={resp.status_code}")

start = time.time()
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]

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

print("time:", time.time() - start)

这里开五个线程同时请求五个接口,每个请求要 2 秒,如果你串行写,要 10 秒左右。但多线程并发,只要等 2 秒多点就搞定了,因为网络等待的时候 GIL 是释放的,其他线程能继续执行。

所以总结一句:Python 的多线程对 I/O 密集型任务依然有用,对 CPU 密集型几乎没戏。

那为什么 Python 要搞个 GIL?

这个就得聊点历史。最初 CPython(最常用的解释器实现)是单线程设计的,为了简化内存管理,作者干脆加了个 GIL,把所有对象的引用计数操作(GC 的一部分)都保护起来,避免出现线程安全问题。

如果没有 GIL,那解释器内部的内存分配、回收就要到处加锁,性能反而可能更差,代码也更复杂。GIL 可以看作是一种简单粗暴但安全的权衡。

那怎么解决 GIL 的限制?

办法还是有的:

  1. 多进程(multiprocessing):每个进程都有自己独立的 Python 解释器和 GIL,这样可以真正利用多核。
  2. C 扩展 / NumPy:很多科学计算库在底层用 C 写的,执行的时候会释放 GIL,多核依然能加速。
  3. 换解释器:比如 Jython(基于 JVM,没有 GIL)、IronPython(基于 .NET)、PyPy(有 JIT 优化,部分情况下更快)。

GIL 的存在就像是一条红绿灯,保证了 CPython 内部不会乱套,但也让多线程在 CPU 密集型场景下慢得要命。如果你要多核并行,就老老实实用多进程或者上 NumPy,别跟 GIL 硬杠。

-END-

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

🔥虎哥私藏精品🔥

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