Python 的 GIL(全局解释器锁)具体如何影响多线程性能?为什么它存在?
很多人刚学 Python 的时候一看到 threading 这个模块,都兴奋得不行,觉得可以像 Java 或 C++ 那样一堆线程一起跑,性能肯定嗖嗖地上去。结果一跑,嗯?怎么还没单线程快?这时候八成就撞上了 GIL。
GIL,全称 Global Interpreter Lock,字面意思就是在 Python 解释器里有一把全局大锁,任何线程想要执行 Python 字节码,都必须先拿到这把锁。换句话说,同一时刻,只能有一个线程在跑 Python 代码。
多线程为什么感觉像“单线程”?
举个例子,你写个多线程程序,开十个线程去算平方:
import threadingdefcpu_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 timeurls = ["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 的限制?
办法还是有的:
多进程(multiprocessing):每个进程都有自己独立的 Python 解释器和 GIL,这样可以真正利用多核。 C 扩展 / NumPy:很多科学计算库在底层用 C 写的,执行的时候会释放 GIL,多核依然能加速。 换解释器:比如 Jython(基于 JVM,没有 GIL)、IronPython(基于 .NET)、PyPy(有 JIT 优化,部分情况下更快)。
GIL 的存在就像是一条红绿灯,保证了 CPython 内部不会乱套,但也让多线程在 CPU 密集型场景下慢得要命。如果你要多核并行,就老老实实用多进程或者上 NumPy,别跟 GIL 硬杠。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领