面试官:你如何破解 Python 的 GIL 的?

作者: 王炳明 分类: Python 高手进阶 发布时间: 2021-09-04 23:41 热度:1,401

1. GIL 是什么?

在讲如何破解 GIL 的时候,首先要和你达成一个共识,那就 Python 中由于有 GIL 的存在,使得 Python 中的多线程即使在有多核的机器上,在同一时刻也只有一个线程在跑。

因此有不少人说,Python 中的多线程是假的多线程。

这个说法,也对,也不对。

说不对,是因为 Python 的解释器有多种,CPython,pypy 等,而上面所说的 GIL 只存在于 CPython 解释器中,只要你换一种解释器, GIL 便不复存在。

说对,是因为在大多数人的认知中,说 Python 大部分都指的是 CPython。

说了那么多,那么 GIL 到底是什么呢?怎么会让 Python 的多线程这么多年都饱受诟病。

GIL ,全称是Global Interpreter Lock,也叫做全局解释器锁。

所有的 Python 线程都需要在解释器这个虚拟机中运行,而在运行之前都要先获取 GIL 这个锁,然后每执行 100 个字节码,解释器就自动释放GIL锁,让别的线程有机会执行。因此即使你有多个 CPU 核,多个线程在同一个 Python 虚拟机中也应该是交替执行的。

2. 解决 GIL 的方法

了解什么是 GIL 后,下一步面试官肯定要问,那你平时都是用什么方法解决 GIL 存在的问题呢?

在这里,我给出自己的一些思考

第一,使用多进程

多个Python进程有各自独立的GIL锁,互不影响。

第二,使用其他 Python 解释器

换其他的 Python 解释器,比如 pypy

第三,仍然使用多线程

因为在 IO 密集型任务中,多线程的『鸡肋』影响不大

第四,使用 C 扩展模块

还有一种就是调用C语言的链接库

3. 如何选择方案?

至于是选择多线程还是多进程,是有一套已成的逻辑存在的,关键要分析你的程序的运行的时间主要花费在哪里。

对于 IO 密集型的程序来说,导致程序运行慢的瓶颈是 IO 的等待,真正会占用 CPU 时间片的并不多,因为对所有面向 I/O 的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待 I/O 的时候运行。

这也意味着,对于 IO 密集型的程序来说,本来用在 cpu 上的时间就很少,使用并行的方式从 10ms 减少到 5ms,其实意义不大,当你用上多核在速度上也不会有太大帮助的时候,使用有点 『鸡肋』的多线程,也许是个不错的选择。

对于 计算密集型的程序来说,你的程序中有大量的计算任务需要占用大量的 CPU 时间,这时候 Python 的假多线程的问题就开始暴露出来了,若执意使用多线程,会导致某个核上非常繁忙而其他核却非常闲的 CPU 时间浪费,这时候上多进程是最佳选择。

聪明的你,现在应该也发现了一个 BUG,多进程不管在 IO 密集型和计算密集型的程序中都能表现良好,那为什么不直接干脆一点,一刀切全部使用多进程呢?

这是个非常好的问题,要回答这个问题,就不得了解一下多进程和多线程的区别?

  1. 线程不能脱离进程单独存在,一个进程里至少有一个线程
  2. 一个进程里的多个线程,可以共享全局变量,通信也比较简单,而多进程不行,需要借助 multiprocessing.Value 将其传入各个子进程进行共享
  3. 创建新线程非常简单,而创建新进程就不一样了,它需要对父进程进行一次克隆,因此它会比线程更占用内存
  4. 一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作子进程
  5. 在不设置守护线程的情况下,主线程退出后,子线程也会同时退出。而如果主进程退出了,不管有没有设置守护,该进程下的所有线程都会退出。但多进程就不一样了,当父进程退出后,若未主动关闭子进程,子进程可以单独存在,处于游离的状态,因此父进程应该最大限制的保护其稳定性。
  6. 可以创建非常多的线程,但进程数却不宜过多,在 IO 密集型的场景下会使用多进程,若该机器上有其他计算密集型的程序在跑,那么你在 IO 密集型的程序中也使用多进程,会增加该任务对 CPU 的抢占能力,会直接影响到其他程序的性能。
  7. 在多个进程之间切换,和在一个进程里的多个线程间切换,开销是不一样的。

由上对比可以看出,若是可以非黑即白地明确你的程序到底是 IO 密集型还是 计算密集型,那么千万要注意在方案的选择,牢记这一原则:IO 密集型选多线程,计算密集型选多进程

上面说的是,非黑即白地明确是哪种程序类型,那假如你的程序的界线并不是那么明显,既有大量 IO 型的,也有大量密集型的呢?

先跟你普及一个知识点,Python 的线程中一旦执行 C 代码,那么 GIL 会怎么释放开来。

因此你可以将你的那些计算密集型任务使用 C 语言来编写,然后把.so链接库内容加载到Python中,这样一来,就算你在多线程中的计算型任务,也可以利用多个核来跑多个线程的目的!

延伸阅读

文章有帮助,请作者喝杯咖啡?

一条评论
  • jianhui-Always-here

    2022年4月8日 下午3:07

    在不设置守护线程的情况下,主线程退出后,子线程也会同时退出
    ————-
    这是设置了守护线程的情况吧

发表评论