线程和 Python 的全局解释器锁
跑路年年有,今年特别多。
咱这不刚好说到“线程”,就得来点技术上的“真话”——尤其是咱们Python程序员,听到“多线程”三个字,总有点想笑又不敢笑的感觉。
为啥?因为GIL这位“搅局王”实在是太出名了。
GIL到底是个啥?
作为一个每天跟Python代码打交道的人,我得实打实说:很多人第一次听说Python的线程模型,都会有点懵。
“Python不是支持多线程的吗?那为啥CPU飙不起来?”
“我都
threading.Thread用了,咋还感觉像是在用单线程?”
这时候,你得看看GIL,也就是 Global Interpreter Lock,全局解释器锁。简单点说,这是Python解释器(尤其是CPython)搞出来的一个“互斥锁”,一次只允许一个线程执行Python字节码。
你开10个线程,它还是让你一个一个来。就像开了10个收银台,但顾客还是得按顺序排队付款,一次只开一个窗口,你说这搞不搞笑?
不过你要骂也不能太狠,毕竟GIL不是来害人的,它是为了简化CPython底层的内存管理逻辑。
因为Python的很多对象管理都不是线程安全的,为了不让你手动加锁加到疯掉,干脆“我给你全局上锁了,你们一个个来”。
这设计思路吧,不能说不好,只能说对“多线程性能”是真不友好。
来段代码感受下“表面风光,实则单干”
import threading
import time
defcount():
n = 0
for _ in range(10**7):
n += 1
start = time.time()
threads = [threading.Thread(target=count) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print("多线程耗时:", time.time() - start)
你会发现,即便你开了4个线程,运行时间也没比单线程好到哪儿去,甚至有时候还更慢。
为啥?因为这些线程在争夺GIL,而GIL让他们一个个排队上CPU,结果你线程间还得频繁切换上下文,额外损耗更高,简直就是“排队吃饭还非要换位子”的经典现场。
那多线程到底还能干嘛?
虽然GIL对“CPU密集型”任务是一大毒瘤,但它对“IO密集型”任务却是友好型选手。
比如你做爬虫、网络请求、文件读写这类操作时,大多数时间CPU是在等IO返回结果,真正用到Python字节码的时间并不多。
这时候,线程可以在IO等待的间隙切出去执行别的任务,效率蹭蹭上去了。
举个IO密集型的例子:
import threading
import requests
import time
deffetch(url):
requests.get(url)
urls = ['https://httpbin.org/delay/1'] * 5
start = time.time()
threads = [threading.Thread(target=fetch, args=(url,)) for url in urls]
for t in threads:
t.start()
for t in threads:
t.join()
print("多线程IO耗时:", time.time() - start)
你看,这段代码会用大概1秒多点完成全部请求,比你串行请求每个URL快得多,原因很简单:虽然GIL还在,但网络IO释放了锁,其他线程可以趁机干活。
想跑得快,得换跑道:多进程才是你的菜
如果你干的是CPU密集型的活,比如数据分析、图像处理、数学计算这类,还是乖乖用multiprocessing吧。
每个进程有自己的Python解释器和GIL,互不干扰,CPU可以跑得飞起。
比如这样:
from multiprocessing import Process
import time
defcount():
n = 0
for _ in range(10**7):
n += 1
start = time.time()
processes = [Process(target=count) for _ in range(4)]
for p in processes:
p.start()
for p in processes:
p.join()
print("多进程耗时:", time.time() - start)
同样是执行4个任务,多进程基本能跑满你4核CPU,跑得贼快。GIL?抱歉,进程之间各自独立,咱不带它玩。
Python你咋这么“保守”?
这时候,有人就想问了:那为啥Java、Go这些语言多线程就没这个破事儿?
说白了就是因为人家底层的内存管理方式和运行时架构不同,不需要像CPython那样依赖GIL来“兜底”。
比如Jython(Python运行在JVM上),就没有GIL。你可以畅快地玩多线程,但你也得自己处理线程安全问题。换句话说,GIL是CPython给“偷懒程序员”的一份“套餐”:你拿到了省心,但也丢了性能。
那GIL会被干掉吗?
多年来,其实社区也一直有声音要“干掉GIL”,甚至有各种提案和实验版本,比如最近的【nogil】项目。理论上说这是有可能的,但实现成本太高,牵一发动全身,简直是“想动GIL,先重写CPython”。
所以短期来看,GIL还得陪咱们过日子。会点并发模型的程序员,也基本默认“IO密集型用多线程,CPU密集型用多进程”,都混熟了这套打法。
面试题:什么是GIL?如何规避其限制?
最优回答如下:
GIL,全局解释器锁,是CPython为保证线程安全引入的机制,它会导致同一时刻只能有一个线程执行Python字节码,限制了多线程在CPU密集型任务中的性能表现。
不过对于IO密集型任务,比如网络请求、磁盘IO,多线程仍然有效,因为线程在等待IO时会释放GIL。
如果处理的是CPU密集型任务,推荐使用
multiprocessing模块,通过多进程实现并发,每个进程有自己独立的GIL,可以充分利用多核CPU性能。
我觉得理解GIL对写高性能Python程序太重要了,它就像你写代码的“天花板”,你不认识它,永远都摸不到它。
只有搞清楚了Python的并发模型,咱才能写出又快又稳的程序——不然就是一个人搬砖,别人开着叉车你还自豪地扛着走,咋说也不合适是不是?