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

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

具体来说,例如放两个线程同时在各自负责的内存空间操作完全不相干的对象时(比如双线程同时计算质数,各自维护各自的资源),那么按照大多数语言的思路,由于没有资源争用,实际上并不需要加锁(反之,如果有争用则必须加锁,目前 py 中也是这么干的),如果这么考量的话,Gil 所谓的有锁线程才允许解释,又解决了什么问题呢,完全没必要不是么
5981 次点击
所在节点    Python
50 条回复
junkun
2021-05-18 21:09:21 +08:00
@ysc3839 None True False 都是不可变的,所以其实没什么问题,因为改变的只是引用。最大的问题是 list, dict, 包括 object 都是没锁的,现在去掉 GIL 改成细粒度锁,这些单进程的性能就会受影响。
ysc3839
2021-05-18 21:50:32 +08:00
@junkun 但是按照 CPython 的规则,这些静态对象也要进行引用计数的,所以多线程同时操作还是会有问题。
ipwx
2021-05-18 21:54:39 +08:00
@neoblackcap 不,tracing gc 前提是有 vm 啊。

但是 python 在 c 扩展方面自由度太大了,根本做不了 vm 啊。。。
====


@LeeReamond 你理解错了,我指的不是 ctypes cffi 这种 C 扩展,而是直接在 C/C++ 项目里面 link python.dll ,主体是 C/C++ 的代码。你甚至可以在 C/C++ 里面直接 Py_ListAppend,或者调用某个对象的 __call__
ipwx
2021-05-18 21:56:21 +08:00
@LeeReamond 或者你可以实现自己的一套引用计数(仿照 C++ std::shared_ptr,直接在你的 struct XXXObject 内部嵌入一个 Py_Object*,在你自己的引用计数清零的时候去调用 python api 清理掉这个 python 对象)。

换句话说,Python 变成了 API,C++ 才是本体。。。从 C/C++ 代码角度看就是这样。
ipwx
2021-05-18 22:00:09 +08:00
@neoblackcap @LeeReamond

比如:

https://zhuanlan.zhihu.com/p/192974017

pybind11,C++ 与 python 互相调用的一个库。

你 C++ 可以有自己的 pthread 线程,甚至用上 uvloop 来做事件驱动,然后控制权可能又进入 python,然后又重新回到 c++。在 C++ 里面如果有多个 pthread native 线程,根本不归 python 管,同时去访问 PyList_Append,这就是 Python 无与伦比的自由度,也是它作为胶水语言的优势,但是也造成了 GIL 尾大不掉。
Jirajine
2021-05-18 22:16:00 +08:00
可以看看 https://rustpython.github.io 有 jit 无 gil,当然还很不成熟,但可以用来做内嵌脚本。
ipwx
2021-05-18 22:54:26 +08:00
@Jirajine yysy qs,解决 GIL 只能靠砍掉 C 模块,拥抱 JIT/VM 。但那就不是 Cython 应该做的事情了。
medivh
2021-05-19 01:14:01 +08:00
三楼因为没有双核 CPU 所以没有并行的那位,求求你了去学点计算机系统基础知识吧...
sagaxu
2021-05-19 01:28:45 +08:00
@medivh 挺好奇的,1C1T 的 CPU 的机器怎么个并行法?
junkun
2021-05-19 01:46:50 +08:00
@ysc3839 理论上来说这些对象都不会被回收,所以计不计数其实影响不大。
ysc3839
2021-05-19 03:01:12 +08:00
@junkun
Python 的文档说要正确处理引用计数 https://docs.python.org/3/c-api/none.html
另外根据 Stack Overflow 上一个回答的说法,Python 是可以复用被释放的对象的 https://stackoverflow.com/a/15288194/6911112
ipwx
2021-05-19 09:47:44 +08:00
@sagaxu 现代操作系统是时间片复用的并行操作系统。也就是说你一个线程比如 PyList_Append 做了一半,操作系统可能就会把这个线程停下来,换另一个线程去执行。因此 1C1T 也有并行,也要加锁。P.S. 这种停下来靠 CPU 硬件中断信号。
sagaxu
2021-05-19 10:01:25 +08:00
@ipwx 分配时间片交替执行,这是并发,不是并行
junkun
2021-05-19 10:21:38 +08:00
@ysc3839 原来如此,但是我还是觉得如果真的要去掉 gil,这些静态对象的计数不是最大的问题。
AlohaV2
2021-05-19 10:24:29 +08:00
Simple is better than complex.

这个跟语言的定位有关系,我觉得我用 Python 的实践是
- 快速实现想法;
- 如果有性能需求,找已有的轮子(比如 numpy 是支持多线程的);
- 如果真有很复杂的需求导致多线程成为瓶颈,那直接在 C++开发,好用的话把它变成一个库(Boost Python);
ipwx
2021-05-19 17:08:51 +08:00
@sagaxu 这无所谓的啦,这种用词我不太在意的。这楼一直在说加锁的事情,那并发(单线程)不用加,但是 1C1T 并行(多线程)就得加。而且你得和他解释为啥 1C1T 这也算并行,有些人还觉得 1T1C 同一时间只有一个线程在跑,不算并行嘞。

所以我都不在意这些用词了。就怕半桶水只知道这种用词,而不知道背后到底具体是什么场景。
ipwx
2021-05-19 17:09:54 +08:00
@junkun 引用计数我看不是大问题,但是 PyList_Append 这种操作可不是原子操作。随便一个线程切换就会让搞到一半的 Append 停下来,另一个线程操作同样的 List,然后就炸了。
ipwx
2021-05-19 17:10:29 +08:00
而且最基础的引用计数可以不枷锁实现,比如 c++ std::shared_ptr,用 atomic 原语就行。
sagaxu
2021-05-19 17:37:33 +08:00
@ipwx 我想表达是,python 出现的年代并行是罕见的场景,IO 密集型场景无所谓 GIL,CPU 密集型场景因为不能并行,无脑加 GIL 和精心打理细粒度锁不会有太大的性能差异。我解释的是 python 选择 GIL 的历史原因,不是说不用加锁,是当时用细粒度锁意义不大。

现在多核多线程乃至多 CPU 是主流了,有了并行了,GIL 在 CPU 密集型场景才凸现出问题。但是因为历史包袱,没法简单的优化掉 GIL 了。

楼上也有人讲过了,就是 gc 不好写,rc 好写,加上当时 GIL 没有害处,自然而然就这么干了。
LeeReamond
2021-05-19 18:20:27 +08:00
@ipwx 所以结合大佬前几个回复,意思是 GIL 保持了类似 C++扩展中在不受 python 控制的情况下仍能并发执行对 python 原生对象的 append 操作,因为 gil 不会释放所以 c++的 pthread 互相之间不会冲突?

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

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

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

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

© 2021 V2EX