>>> from env_helper import info; info()
页面更新时间: 2023-12-27 10:46:04
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

6.14. 理解GIL的局限性

在Python多线程编程中,你有没有遇到过这种问题:多线程Python程序运行的速度比 只有一个线程的时候还要慢?除了程序本身的并行性之外,很大程度上与GIL有关。GIL在 Python中是一个很冇争议的话题,由于它的存在,多线程编程在Python中似乎并不理想, 为卜么这么说呢?先来了解一下GILU GIL被称为为全局解释器锁(Global Interpreter Lock), 是Python虚拟机上用作互斥线程的一种机制,它的作用是保证任何情况下虚拟机中只会有一 个线程被运行t而其他线程都处于等待GIL锁被释放的状态。对于有I/O操作的多线程,其 线程执行状态如图6-6所示。不管是在单核系统还是多核系统中,始终只有一个获得了 GIL 锁的线程在运行,每次遇到I/O操作便会进行CIL锁的释放。

但如果是纯计算的程序,没有I/O操作, 解释器则会根据sys.setcheckinterval的设置来 自动迸行线程间的切换,默认情况下每隔100个时钟(注:这里的时钟指的是Python的内 部时钟,对应于解释器执行的指令)就会释放GIL锁从而轮换到其他线程的执行,示意图如 图6-7所示

_images/img8.png

在单核CPU中.GIL对多线程的执行并没有太人影响.因为单核上的多线程本质上就是 顺序执行的。佴对于多核CPU,多线稈并不能真正发挥优势带来效率上明显的提升,甚至在 频繁I/O操作的情况下由于存在需要多次释放和申请GIL的情形,效率反而会下降。那么,有 人不禁会问:Python解释器中为什么要引人GIL呢?来思考这样一个情形:我们知道Python 中对象的管理与引用计数器密切相关,当计数器变为0的时候,该对象便会被垃圾回收器回 收。.当撤销对一个象的引用时,Python解释器对对象以及其计数器的管理分为以下两步:

1)使引用计数值减1。

2)判断该计数值是否为0,如果为0,则销毁该对象。

假设线程A和B同时引用同一个对象obj,这时obj的引用计数值为2。如果现在线程 A打算撤销对obj的引用。当执行完第一步的时候,由于存在多线程调度机制,A恰好在这 个关键点被挂起,而B进入执行状态,如图6-8所示。

_images/img9.png

但不幸的是B也同样做了撤销对obj 的引用的动作,并顺利完成了所有两个步骤,这个时候由于obj的引用计数器为0,因此对象被销毁.内存被释放。但如果此时A再次被唤醒去执行第二步操作的时候会发现已经面目 全非,则其操作结果完全未知。

鉴于此.在Pylhon解释器中引人了 GIL,以保证对虚拟机内部共享资源访问的互斥性, G1L的引人确实使得多线程不能在多核系统中发挥优势1但它也带来了一些好处:大大简化 了 Python线程中共享资源的管理,在单核CPU上.由于其本质是顺序执行的一般情况下 多线程能够获得较好的性能。此外,对于扩展的C程序的外部调用,即使其不是线程安全 的,但由于GIL的存在,线程会阻塞直到外部调用函数返回,线程安全不再是一个问题。

多核CPU已经成为一个常见的现象,GIL的局限性限制了其在多核CPU上发挥优势, 因此对于GIL的去留也曾引发过激烈的讨论4 Guido以及Python的开发人员都有一个很明确 的解释,那就是去掉GIL并不容易。实际上在1999年,针对Python1.5. Gireg Stein发布了 —个补丁,该补丁中GTL被完全移除,使用高粒度的锁来代替。然而这种解决方案并没有带 来理想的效果,多核多线程速度的提升并没有随着核数的增加而线性增长,反而给单线程程 序的执行速度带来了一定的代价,当用单线程执行时,速度大约降低了 40%。因此,这种方 案最终也被放弃。在Python3.2中重新实现了 GIL,其实现机制主要集中在两个方面:一方 面是使用固定的时间而不是固定数量的操作指令来进行线程的强制切换;另一个方面是在线 程释放GIL后,开始等待,直到某个其他线程获取GIL后,再开始去尝试获取GIL,这样虽 然可以避免此前获得GIL的线程,不会立即再次获取GIL,但仍然无法保证优先级髙的线程 优先获取GIL。这种方式只能解决部分问题.并未改变GIL的本质,G1L本质上的改观目前 并没有非常明朗的前录。不过也不需要那么悲观,Python提供了其他方式可以绕过GIL的局 限,比如使用多迸程multiprocessing模块或者采用C语言扩展的方式,以及通过ctypes和C 动态库来充分利用物理内核的计算能力。