Python 的 gil 到底解决了什么具体的问题?

2021-05-17 23:52:52 +08:00
 LeeReamond
如题,用了这么多年 py,Gil 按理说应该很熟悉了,但是今天被问到 Gil 具体锁了哪些东西的时候却被问住了。毕竟虽然引入了 gil 机制,但 py 中的线程争用资源由于原子性问题仍然需要用户自行上锁,细究的话很多文章讲的加入 Gil 后避免细粒度锁其实是不对的,因为用户层面并没有实现无锁感知。

具体来说,例如放两个线程同时在各自负责的内存空间操作完全不相干的对象时(比如双线程同时计算质数,各自维护各自的资源),那么按照大多数语言的思路,由于没有资源争用,实际上并不需要加锁(反之,如果有争用则必须加锁,目前 py 中也是这么干的),如果这么考量的话,Gil 所谓的有锁线程才允许解释,又解决了什么问题呢,完全没必要不是么
5763 次点击
所在节点    Python
50 条回复
ipwx
2021-05-18 01:12:24 +08:00
1. Python 解释器层面的一些锁。这个其实想要优化总能优化的。
2. 保护 C 扩展模块,这方面是挺蛋疼的。

Python 生态半壁江山是 C 扩展库撑起来的,丢掉这部分江山等于自废武功。为了支持无锁而去掉 GIL 等于丢掉所有这些库,做网络应用的是爽了,但是 Python 就分裂了。
ipwx
2021-05-18 01:20:01 +08:00
说的更明确一些:哪怕 C 扩展库对自己的逻辑进行了加锁,但是由于要访问 Python 对象,进一步使用 Python 解释器的东西,它防止不了外面的代码同时访问相同的对象,然后就崩溃了。。。

所以面对 C 扩展库,GIL 喊了这么长时间都搞不掉。
sagaxu
2021-05-18 01:20:34 +08:00
python 出道的时候(90 年代初),民用 CPU 都是单核单 CPU,十几年后才有了双核 CPU 。因为没有并行,自然而然的选择了引用计数加 GIL,实现简单又不会太影响性能。后来积攒了大量的 C 扩展,保留兼容性的同时去掉 gil 是极其困难的事情,况且必要性不高,性能不够拿扩展来凑就是了。
ipwx
2021-05-18 01:24:26 +08:00
…… 所以其实 Python 成也“胶水”败也“胶水”。C 扩展库能这么容易访问 Python 底层的东西是个优势,可以迅速做一些系统层面的 API 调用、融合 C/C++ 做科学计算( PyTorch,NumPy,或者 GPU 计算),或者在暴露 C/C++ 写的核心算法为 HTTP API 。但是同样的,这个优势也让 Python 在普通的多线程网络服务器上寸步难行。

我的看法是,Python 干好科学计算这方面的事情就行了。还有就是干好工具型胶水语言该做的事情,在性能无关的领域放光(比如运维)。科学计算本来就计算密集型,本来就要独占核心一直跑,根本不会有线程上下文切换。网络服务交给专业的,比如 Go,Java,C++ 什么的。多好。。。
roundgis
2021-05-18 01:58:57 +08:00
推薦看沈崴的回答
LeeReamond
2021-05-18 03:40:41 +08:00
@ipwx 据我所知 libffi 几种调用方式,一种是将数据复制一份由扩展自行维护,比如 numpy 就自己一种储存结构,另一种是传入 python 数据结构的指针。所以你的意思是 GIL 防止的问题是一个 python 解释线程在操作(比如一个 python 列表)的数据结构时,另一个线程中的 C 扩展同时修改该结构?不过这又有什么意义,扩展中可以手动释放 GIL
dreampuf
2021-05-18 05:24:08 +08:00
https://softwareengineering.stackexchange.com/questions/186889/why-was-python-written-with-the-gil

> Benefits of the GIL
> - Increased speed of single-threaded programs.
> - Easy integration of C libraries that usually are not thread-safe.
> ... This lock(GIL) is necessary mainly because CPython's memory management is not thread-safe.
raysonx
2021-05-18 06:46:58 +08:00
严格来说,GIL 是 CPython 解释器的历史遗留问题,而不是 Python 这门语言自身的问题。
ysc3839
2021-05-18 07:06:41 +08:00
> 但 py 中的线程争用资源由于原子性问题仍然需要用户自行上锁

用 CPython 真的要自己加锁吗?我似乎没见到什么项目这么做的,能否举个例子?

> 在各自负责的内存空间操作完全不相干的对象时……实际上并不需要加锁

但是 CPython 很多东西是全局共用的,并不是线程独立的,所以这说法不成立。
liuxingdeyu
2021-05-18 09:53:25 +08:00
我搬运一下维基百科的说法


CPython 的线程是操作系统的原生线程。在 Linux 上为 pthread,在 Windows 上为 Win thread,完全由操作系统调度线程的执行。一个 Python 解释器进程内有一个主线程,以及多个用户程序的执行线程。即便使用多核心 CPU 平台,由于 GIL 的存在,也将禁止多线程的并行执行。[2]
Python 解释器进程内的多线程是以协作多任务方式执行。当一个线程遇到 I/O 任务时,将释放 GIL 。计算密集型( CPU-bound )的线程在执行大约 100 次解释器的计步( ticks )时,将释放 GIL 。计步( ticks )可粗略看作 Python 虚拟机的指令。计步实际上与时间片长度无关。可以通过 sys.setcheckinterval()设置计步长度。
在单核 CPU 上,数百次的间隔检查才会导致一次线程切换。在多核 CPU 上,存在严重的线程颠簸( thrashing )。
Python 3.2 开始使用新的 GIL 。新的 GIL 实现中用一个固定的超时时间来指示当前的线程放弃全局锁。在当前线程保持这个锁,且其他线程请求这个锁时,当前线程就会在 5 毫秒后被强制释放该锁。
可以创建独立的进程来实现并行化。Python 2.6 引进了多进程包 multiprocessing 。或者将关键组件用 C/C++编写为 Python 扩展,通过 ctypes 使 Python 程序直接调用 C 语言编译的动态链接库的导出函数。
参考文献
GlobalInterpreterLock - Python Wiki. [2018-07-19]. (原始内容存档于 2018-06-23 ) (英语).
David Beazley. Inside the Python GIL (PDF). Chicago: Chicago Python User Group. 2009-06-11 [2009-10-07]. (原始内容存档 (PDF)于 2010-12-24 ).
LeeReamond
2021-05-18 09:58:38 +08:00
@liuxingdeyu 解释切换长度还有 api 可调,学习了。不过似乎并未回答主题关心的问题。
LeeReamond
2021-05-18 10:14:12 +08:00
@ysc3839 你这么一说的话倒是确实,sys.modules 之类的环境倒是确实是共享的,应该保护起来。不过我感觉也不是充分条件,毕竟用户代码未必需要修改他们,少数修改的情况用 hook 的方式可以解决,没必要为了他们搞一个全局大锁。至于用户加锁我想应该不用举例,比如多线程共享某 int 对象执行 x+=n 操作,虚拟机指令会翻译为 LOAD_FAST 、LOAD_CONST 、INPLACE_ADD 、STORE_FAST 四部,且可在任意中间位置打断,显然需要锁保护。只不过因为 py 的线程锁粒度大且不好控制,确实很少见。
generic
2021-05-18 12:13:11 +08:00
GIL 锁住了什么?
字节码解释循环。多个线程在任意时刻,只有一个线程能运行字节码,除非字节码调用的 C 扩展显式释放了 GIL 。

GIL 的作用:
防止多线程的字节码恰好访问同一 Python 对象造成 crash 。GIL 使得多线程的纯 Python 程序不可能(因为 race condition )在 VM 的 C 代码里 crash 。
如果 C 扩展要显式释放 GIL,那么该扩展就需要自行保证线程安全。

如果直接去掉 GIL 会怎样:
多线程 Python 程序的 bug 会 crash 在 VM 的 C 代码里。
ipwx
2021-05-18 13:00:17 +08:00
@LeeReamond 参见楼上 “如果 C 扩展要显式释放 GIL,那么该扩展就需要自行保证线程安全”
ipwx
2021-05-18 13:06:15 +08:00
@LeeReamond ……接楼上。

如果 Python 一开始就决定没有 GIL,那么每个 C 扩展库作者必须知道每个版本的 Python 内部的操作是怎么回事,小心地去处理每一次和 Python 对象的打交道。这个心智负担就太重了。肯定不能完全依赖于 C 扩展库的作者。

Java/C# 因为有 VM 所以可以大大简化这里的问题,但是 Python 因为允许完全不受管理的 C 扩展所以。。。GIL 是当年和现在都最容易的选择,也是现在很难解决的问题。
ysc3839
2021-05-18 14:11:08 +08:00
@LeeReamond 关于锁的问题多谢指正,是我没开发过多线程相关的,所以以为不需要。

关于共享的东西,CPython 的 None True False 是全局共享的,这三样东西是很难绕开的。
Austaras
2021-05-18 14:24:23 +08:00
不是,说这么多怎么没人总结一下,就是 gc 好写。。。
anzu
2021-05-18 14:34:40 +08:00
我觉得只是 kiss
neoblackcap
2021-05-18 16:46:25 +08:00
GIL 的问题是由于当时采用引用计数这样的垃圾回收技术引出的。毕竟要保护好引用数正确,要不就是处处是小锁。要不就是一个大锁(GIL)。当时为了单线程的性能,就用了一个大锁( GIL )。
后面技术发展,大家发现其实 tracing-base 的垃圾回收方案速度也不慢,同时各种优点。于是现代化的 VM 都是 tracing gc 了。
要去掉 GIL,那是得大改啊。
LeeReamond
2021-05-18 20:00:10 +08:00
@ipwx 感谢回复,所以意思是关于 ffi 交互方面,按照原始的 gil 设计,是希望让库设计者在设计库时不必考虑多线程同时操作 python 对象的问题,从而简化 C 扩展的编写难度,后期也是一直为了兼容这个目标,是这个意思吧。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/777560

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX