全局解释器锁(GIL)
# 全局解释器锁(GIL)
Python 中要使用多线程和线程锁,那一定会涉及到 GIL 的知识点,这样是 Python 这门语言在多线程处理的特殊之处。
# 前言
在大多数环境中,单核 CPU 情况下,本质上某一时刻只能有一个线程被执行。多核 CPU 则可以支持多个线程同时执行。
但是在 Python 中,无论 CPU 有多少核,同时只能执行一个线程。这是由于 GIL 的存在导致的。
GIL 的全称是 Global Interpreter Lock(全局解释器锁),是 Python 设计之初为了数据安全所做的决定。Python 中的某个线程想要执行,必须先拿到 GIL。可以把 GIL 看作是执行任务的「通行证」,并且在一个 Python 进程中,GIL 只有一个。拿不到通行证的线程,就不允许进入 CPU 执行。GIL 只在 CPython 解释器中才有,因为 CPython 调用的是 C 语言的原生线程,不能直接操作 CPU,只能利用 GIL 保证同一时间只能有一个线程拿到数据。在 PyPy 和 JPython 中没有 GIL。
# Python 多线程的工作流程
- 拿到公共数据
- 申请 GIL
- Python 解释器调用操作系统原生线程
- CPU 执行运算
- 当该线程执行一段时间消耗完,无论任务是否已经执行完毕,都会释放 GIL
- 下一个被 CPU 调度的线程重复上面的过程
# Python 多线程还有用吗
Python 针对不同类型的任务,多线程执行效率是不同的。
- 对于 CPU 密集型任务(各种循环处理、计算等等),由于计算工作多,ticks 计数很快就会达到阈值,然后触发 GIL 的释放与再竞争(多个线程来回切换是需要消耗资源的)。所以 Python 下的多线程对 CPU 密集型任务并不友好。
- 对于 IO 密集型任务(文件处理、网络通信等涉及数据读写的操作),多线程能够有效提升效率(单线程下有 IO 操作会进行 IO 等待,造成不必要的时间浪费,而开启多线程能在线程 A 等待时,自动切换到线程 B,可以不浪费 CPU 的资源,从而能提升程序执行效率)。所以 Python 的多线程对 IO 密集型任务比较友好。
- ticks 可以看作是 Python 自身的一个计数器,专门作用于 GIL,每次释放后归零,这个计数可以通过
sys.setcheckinterval
来调整。- Python2.x 里,GIL 的释放逻辑是当前线程遇见 IO 操作或者 ticks 计数达到 100;python3.x 中,GIL 不使用 ticks 计数,改为使用计时器即执行时间达到设定阈值后,释放当前线程的 GIL。
并且由于 GIL 锁存在,Python 里一个进程永远只能同时执行一个线程(拿到 GIL 的线程才能执行),所以在多核 CPU 上,Python 的多线程效率并不高,倒不如使用多进程了。
# 为什么不能去掉 GIL
首先,在早期的 Python 解释器依赖较多的全局状态,传承下来,使得想要移除当今的 GIL 变得更加困难。
其次,对于程序员而言,仅仅是理解 GIL 的实现就需要对操作系统设计、多线程编程、C语言、解释器设计和 CPython 解释器的实现有着非常彻底的理解,更不用说对它进行修改删除了。
总之,整体技术难度大,会对当前内部框架产生根本性的影响,牵一发而动全身。
虽然,在 Python 的不同解释器实现中,如 PyPy 就移除了 GIL,其执行速度更快(不单单是去除 GIL 的原因)。但是,我们通常使用的 CPython 解释器版本占有着统治地位的使用量。
# 实际使用中的建议
Python 中想要充分利用多核 CPU,就用多进程。因为每个进程有各自独立的 GIL,互不干扰,这样就可以真正意义上的并行执行。在 Python 中,多进程的执行效率优于多线程(仅仅针对多核 CPU 而言)。同时建议在 IO 密集型任务中使用多线程,在计算密集型任务中使用多进程。
另外,也可以深入研究 Python 的协程机制。
(完)