Python 的多线程原来不是真的多线程啊

2019-11-20 10:33:19 +08:00
 vcfghtyjc

用了好几年 Python 今天第一次知道 Python 的多线程其实是顺序执行各个线程。真正并发还是要靠多进程,或者通过 Cython 调用 C/C++ 的库。

总的来说就是每个线程都需要获取 GIL 后才能执行。具体可以看链接

除了这个以外,各位还有什么在 Python 里踩过的坑吗?可以交流一下。

13102 次点击
所在节点    Python
55 条回复
lewinlan
2019-11-20 15:30:06 +08:00
刚入门的时候也被坑了,甚至一度认为“一个核只能运行一个进程”,笑哭。
学学操作系统,再学个 Golang,你会知道除了线程还有协程在等着你。

至于楼上说用其他解释器的,我认为是纯粹炫技。Python 终究是个脚本语言,别太认真
rubycedar
2019-11-20 15:35:18 +08:00
村通网?...
daimiaopeng
2019-11-20 16:02:40 +08:00
多线程还是调用 boost python 自己做的 c++线程舒服
neoblackcap
2019-11-20 16:38:25 +08:00
@daimiaopeng 用 pybind11 吧
lolizeppelin
2019-11-20 16:40:32 +08:00
不要把逻辑纠结到名词上,名词是为了表意而已,代码本身脱离不了人类逻辑

34 楼说了关键,能不能并发(这个词本身就不应该纠结),需要逻辑上可行!
单关键点不是在 34 楼所说的共享,关键点在于,多个 work/thread/process/runner/loop (用哪个名词真不重要)之间的工作是否有序
你要知道一旦执行过程的逻辑是有序的, 那么最终无法一起做或者只能片段一起做(部分无序部分可以同时进行),即使你分配到了多个线程,最终顺序还是要用锁来保证,结果还是强制了顺序,即使看上去用了多个 cpu,实际都浪费到锁上


python 的线程是真线程, 但是 GIL 要求 python 代码片段被顺序执行,所以多个线程并没有同时干活,也就是没法用多核,最多跑 100%.

你的 python 代码是跑 python 虚拟机里的,GIL 就相当于 python 虚拟机里有个强制的线程顺序锁,你纯 python 代码怎么写都没法脱离他的顺序限制,自然是无法达成你需要的并发的

所以要么写 C 代码脱离 python 虚拟机的 GIL 限制,要么多进程
zappos
2019-11-20 17:17:42 +08:00
单核上的线程本来就没办法做到并行。

多核上的可以,但你要考虑 python 诞生的时候牙膏厂还没开始堆核。在那个时候有没有 gil 是看不出来区别的。

都是历史的眼泪。
kevinmissu
2019-11-20 18:09:15 +08:00
怪不得 每次面试都会问多线程多进程,大家理解都不一样 ,看到理解一样的 就觉得这个小伙子不错跟我理解一样,就招了, 要是不一样,不好意思 回去等通知~~~~~~娱乐一下 哈哈哈
reus
2019-11-20 19:03:27 +08:00
@est 除了 go,主流语言的线程基本都是系统线程。
Leigg
2019-11-20 19:20:56 +08:00
是并发,只是单核并发,在多 io 任务是有效果的。
secondwtq
2019-11-20 20:03:55 +08:00
@lolizeppelin 怎么不应该纠结名词 ... 特定的名词在特定的上下文里有特定的意思,相近不等于相等。语言背后的含义当然是最重要的,但是不准确的语言无法准确表达意思

这个帖子的大多数内容其实都是由语言的问题引发的,从楼主的标题开始,就没有能准确地传达意思。其实大家(可能要排除发这贴之前的楼主 ...)都知道楼主标题要表达什么意思,毕竟是 Python 一个 well-known 的坑,但是大家也都在挑楼主”不是真正的多线程“语句上的毛病 ... 以及楼主说的”真正并发还是要靠多进程“ ... 从楼主的回复看楼主是明白一些(英文)名词的,只不过 map 到中文不大会,以及可能发帖的时候没太注意
这一切都恰好说明了把话从一开始说准,说对是非常重要的

我预感此楼可能要 @FrankHB ...
lolizeppelin
2019-11-20 20:29:13 +08:00
@secondwtq

相近但是不常用的词没法准确传递的
最明显的就是并行和并发, 这两词具体表达什么意思是“定义”下来而不是可直接理解推导的

今天我看了记得定义了明天就忘记了, 那些本来就不熟悉这些内容的更加记不住了

所以用词得绕着表达, 比如利用多核这种可以直接理解的
clino
2019-11-20 20:34:37 +08:00
go 的 coroutine 都能用多核,这个应该是很大的优势
其他语言的 coroutine 应该都没有吧?
secondwtq
2019-11-20 20:45:48 +08:00
@lolizeppelin 那按照同样的逻辑,你也不应该用”能不能并发“这种”无法准确传递“的词啊 ...

还有另一个问题是 parallelism/concurrency 的区别应不应该是程序员的常识,也就是说 parallelism/concurrency 到底能不能”准确传递“
我个人是觉得一个合格的程序员是应该能够区分 concurrency 和 parallelism 的,虽然事实上很多人并不能做到——对于我来说,这只能说明他们暂时还不是合格的程序员,如果他们搞不明白,多学习一下就行了,随便 google 一下相关资料很多。我并不会因为这种原因就不用甚至混用这两个词(混用是比不用更大的恶)
或者说大家都想要所有的东西都很直白随便看看都能看懂,不用理解定义,不用纠结细节,甚至不用思考,任凭劣币驱逐良币,坚决贯彻落实 Worse is Better,我觉得像什么 C 啊 Golang 的忠实拥趸应该会很高兴

或者说,Python 程序员一般不能区分,Golang 程序员一般可以区分
毕竟 Rob Pike 做过一个 Talk 就叫 Concurrency is not Parallelism
查一下还能找到相关的讨论: https://news.ycombinator.com/item?id=9450016
Balthild
2019-11-21 16:31:27 +08:00
@clino 这个要看具体的调度器怎么设计。比如 Rust 里面,你使用不同的 executor,最终结果是在单核还是多核上跑也是不一样的。
FrankHB
2019-11-23 00:43:59 +08:00
所以这些 CS 导论就该明确方向的系列问题怎么还那么经……
还是得从基础概念入手。
https://stackoverflow.com/a/51759235/2307646
里面引用的 Robert Harper 的博客文章看来又能点进去了,那就不重复为什么需要这样明确之类的细节问题了。
就说重点,补课,再解释顶楼的问题:
1.并行(parallelism) 和并发(concurrency) 都是计算(或者说,表达计算的程序片段)的性质,但两者本质上是两回事。
更进一步地,两者可以是正交的:程序中并行和并发程度的多少之间也没有必然联系。
注意,计算是抽象的。计算的实现,包括“同时”之类的物理属性,和这些性质没有直接关系。
2.并行是关于管理程序中确定性的(deterministic) 计算之间的依赖(dependency) 使它们整体具有更好的渐进效率(asymptotic efficiency) 的性质;并发是关于处理程序的各种非确定性的组合(non-deterministic composition) 使之能响应各种不保证可准确预知的时机输入的性质。
一定程度上,不考虑依赖之间的动态变化,并行描述的是程序中的计算之间的静态关系,而并发可以描述不确定的动态关系。
3.并行的对立面是串行(seriality) ,指一系列确定的计算明确具有链式的依赖;并发的对立面是序列(sequentiality) ,指不确定的计算的某个子集之间,其顺序被约束了。
约束计算的操作称为调度(scheduling) ,其中可能包含对计算资源的分派(dispatch) 。
4.进程(process) 或者线程(thread) 为了实现是占用特定的计算资源完成的带目的计算(或者说,任务(task) )引入的抽象,是程序的动态映像和为了实现计算分配的其它资源的集合。
两者的区别传统上和资源的具体集合有关,这里不是重点,以下都以可调度的线程代表。
作为组成其它程序的组件,它们可具有并行和并发的性质,分别通过竞争性调度机制的具有非确定性和调度中分派计算资源的目标不同的事实而体现。一般地,决定如何调度的逻辑是在进程或者线程之外的。程序通过这样的抽象的表达,只是表明计算已被拆分成为不同的待调度的组件,并不能保证已经具有并行或者并发的性质。
5.使用进程或者线程这样的抽象只是为了便于实现这种特定的可调度的计算表达。
这不排除其它手段实现并发和并行的手段,因为一般情形下,调度自身的效果被不确定性涵盖而不是计算的作用(effect) ,不被要求对用户可见。最简单地,一个表达式在计算时若不要求子表达式计算之间的相对顺序,也没有影响计算结果的依赖限制它们之间的顺序(以保证不违反计算的因果性语义),那么它们的计算原则上就是并发的——用户没法预知确定的计算如何发生,也没法保证确定这里是不是需要插入一个线程或者其它可调度的实体来实现并发;而若使用确定的最优化规则(例如某个指令集允许的指令级并行规则),它们之间也可以是并行的,用户可以查看目标代码等方式来确认这里的计算被分派到不同的设备上同时实现以取得比串行计算更好的效率。
6.为了解决共享资源的竞争引起破坏计算的目的非预期的非确定性,可能需要引入同步。
同步操作和调度一样约束计算的顺序,因此可能有相互作用,例如增加不必要(程序中不要求表达)的隐含依赖而减小程序的可并行部分,并最终实际减小并行程度。
就 LZ 的例子,GIL 是一种粗粒度的内部调度机制,在极端情况下把并行语义的程序串行化。这个意义上它是不并发的,但这是偶然情况,不是实现系统总是对用户可见的整体的性质,更不是语言要求的性质( PyPy 就不用 GIL )。
7.多线程中的线程在不同的上下文中不严格地是一回事。
一般地,高级语言使用表达式的求值引入计算的作用。对大多数高级语言来讲,默认情形表达的计算遵循同样的顺序规则,最常见的如指令式语言按程序的字面顺序(literal order) 。这个顺序整体上约束了确定性计算(尽管按上面的讨论,像子表达式这样的计算仍然可以是非确定性的)。
为了更明确地表达可被程序操作的非确定性计算,同时不修改默认情形的计算顺序的语义,按原有规则的计算任务整体被抽象为一个执行线程(thread of execution) 。多线程环境即指语言允许程序蕴含多个执行线程,每个线程内都适用默认计算规则;而线程间是没有类似的计算顺序规则限制,需要再另行约定共享计算资源和同步操作以明确线程之间计算的互操作。
这个意义下,“同时”或者竞争调度不是多线程的关键。尽管语言中的执行线程允许并发,但不可见的调度实现不需要提供计算资源是否在物理上(同时)被重叠利用的保证。极端地,调度可以完全放弃对线程的资源分派,无限等待线程自发完成计算,此时线程不再抢占(preemptive) ,调度退化为协作式多任务(cooperative multitasking) 的实现。
但大多数情形在语言引入执行线程的目的不止是为了允许在程序中表达非确定性,还同时作为提供显式复用计算资源的特性(乃至显式地并行),所以不太会有这种调度内部直接放弃非确定性退化成平凡情况,执行线程在这里也就和一般意义上被实现调度的线程不加区分,统称为线程。
存在 GIL 虽然更接近退化的情况,只要不改变被实现的语言具有多个执行线程,且语言不能绕过执行线程可见地调度线程,那么整个实现就是一个多线程环境。
8.在硬件实现中,因为在外部看来计算资源整体的占用是很大程度上能预期上限,习惯上把这部分实现可用的资源(特别地,一个 CPU 核)作为一个物理线程。相对地,一个能被软件分辨并调度的线程是逻辑线程。
同时多线程(SMT) 专指这里的复用物理处理器核的硬件资源,以使一个物理处理器支持超过一个逻辑线程的手段。在软件看来,一个线程逻辑上对应一个处理器,因此一个物理核心对应多个逻辑处理器。
不支持 SMT 且只有一个物理核心的实现仍然可以通过分时复用实现多线程。这和软件的语言实现的情况类似,因为物理核心原则上对软件不可见,所以软件的意义上这就是“真正的”多线程。
9.硬件相比软件的特殊性是可提供物理同时的多任务支持:一个处理器 package 可以有多个物理核,如同一个抽象机进程中支持多个执行线程,这似乎符合最朴素的抽象。
但这和并行或者多任务根本上都没有实现以上的关系,因为如最开始提到的,“物理同时”从来就不是考虑这些抽象时必备的要素,仅仅是方便理解而提到罢了。
而且,实际的处理器内部也不可能完全重叠地利用计算时间,最显然地,要求时钟同步来避免非预期的不确定性。
此外,单一核仍然能通过异步中断能实现物理上同时的多任务,这和多线程也没有直接的关系。

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

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

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

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

© 2021 V2EX