用了好几年 Python 今天第一次知道 Python 的多线程其实是顺序执行各个线程。真正并发还是要靠多进程,或者通过 Cython 调用 C/C++ 的库。
总的来说就是每个线程都需要获取 GIL 后才能执行。具体可以看链接
除了这个以外,各位还有什么在 Python 里踩过的坑吗?可以交流一下。
抱歉有些没表述清楚的地方。我的意思是 Python 标准库里的多线程无法做到Parallel。
关于多线程的定义,多谢各位大佬指正。这里附一个 Wiki 的图。
1
hdbzsgm 2019-11-20 10:37:17 +08:00
你可以用没有 GIL 的解释器
|
2
scukmh 2019-11-20 10:38:35 +08:00
不是顺序执行多个线程,只是其他语言在系统线程上运行只需要有 cpu 执行权就可以了。python 还多要一个 gil.
|
3
binux 2019-11-20 10:39:04 +08:00 via Android 6
GIL 和是不是真线程无关。
|
4
xpresslink 2019-11-20 10:39:32 +08:00 1
从概念上说是真的线程。只是在 IO 密集场景下才有实质用处,IO 操作时线程会主动释放 GIL。但是在 CPU 密集场景 GIL 起主导作用。记住这一句基本上就够了。
|
5
676529483 2019-11-20 10:42:08 +08:00
本质不是多线程的错,python 的多线程和 c++的实现方式都是调用操作系统的接口,只是 GIL 对此做了限制。另外也不是完全没用,GIL 在碰到 IO 阻塞的时候,会释放 GIL,因此 IO 操作会比串行快一些
|
6
BBCCBB 2019-11-20 10:45:55 +08:00
一个进程的话只能交替执行呀
|
7
ipwx 2019-11-20 10:50:19 +08:00 via Android 1
是真线程,只不过有 gil 的情况下,除了调用 c 库(这比你想象的多)和 io 操作,只有一个线程能运行。
|
8
tt67wq 2019-11-20 10:51:36 +08:00
你说的是并发还是并行?
|
9
est 2019-11-20 10:52:38 +08:00
python 的 多线程就是真正的 os thread。其他语言才是假的 user thread/fiber
|
10
laike9m 2019-11-20 10:58:34 +08:00 via Android
Python 的多线程当然能实现(任务级别)的并发,你想说的是并行
|
11
vcfghtyjc OP @tt67wq 不太清楚 Parallel 和 Concurrent 哪一个是并发,哪一个是并行。我想说的是:不能做到 Parallel
|
13
est 2019-11-20 11:05:26 +08:00
@vcfghtyjc python 怎么就不能做到 parallel 了。GIL 只能导致你没法有效利用多核 CPU 而已。然而市面上一大堆 C++ 游戏也只能跑满一个核啊。你们把 pthread 也拿出来批判一番?
|
14
wangxin13g 2019-11-20 11:05:46 +08:00
@est 借楼问一下 java 的线程是 os thread 么,没写过 java 但是按照之前的了解 好像 Java 用的系统层面的 epoll 来实现多线程?
|
15
ipwx 2019-11-20 11:05:46 +08:00 via Android
|
16
hhhsuan 2019-11-20 11:08:02 +08:00
是真的,如假包换的多线程。
|
17
vcfghtyjc OP @est 没说 python 不能 parallel,而是 python 标准库里的多线程不能做到 parallel。抱歉没表达清楚。
|
18
jimages 2019-11-20 11:13:19 +08:00
3.9 出来,子解释器出来,就可以愉快的使用多线程了。
|
19
vcfghtyjc OP @ipwx Multiprocessing 是可以实现 Parallel, 但是进程之间不能共享内存,这不会让消息传递的 overhead 过大吗?
|
20
guokeke 2019-11-20 11:29:55 +08:00
真正并发需要靠多核,单核多进程还不是轮流执行进程。
|
21
dbow 2019-11-20 11:30:33 +08:00
GIL 保证解释器同时只解释一个 py 线程的代码, 但是如果这个线程 IO 阻塞了, 解释器就跳到其它线程。 多线程是真的, 只不过锁太大。
|
23
claymore94 2019-11-20 11:33:01 +08:00
其他语言的多线程不也是一个核上只有一个线程在运行么?
|
25
ipwx 2019-11-20 11:35:21 +08:00 via Android
@vcfghtyjc 因为你没有指定场景,我们假设是 web app。这种环境不需要 cross process message passing,mp 够了。其次,这种场景基本都是 io,所以其实连 python 多线程也能并发。
当然你可以补充说明一下你的应用场景。 |
26
vcfghtyjc OP @claymore94 应该不是吧,单进程多线程是可以跑在所有核上的
|
27
ipwx 2019-11-20 11:37:29 +08:00 via Android
@vcfghtyjc 这真的要看场景。比如我主线程运行 tensorflow session.run ,由于它内部会释放 gil,那我就可以在另一个标准 Python 线程里面,用 python 代码预读下一个 minibatch 的数据。你认为这是不是真的多线程并行?
|
28
vcfghtyjc OP @ipwx 确实按照通常来讲 web app 是最常见的场景。但是我这里的场景是进 /线程间高通信,高计算,低 IO。
|
30
ipwx 2019-11-20 11:43:50 +08:00 via Android 1
@vcfghtyjc 高计算场景纯 python 一开始就 out 了,根本连谈论多线程的资格都没有。带 c 库的(比如 numpy, pandas),或者上 cython/numba/pytorch/tensorflow 的,内部都会释放 gil。另外你 mp 库总得用个 c 库的吧,比如 mpi 之类的。一堆 c 库和 io 加持,多线程有啥问题?
|
31
ipwx 2019-11-20 11:45:15 +08:00 via Android
@vcfghtyjc 而且这种计算,多线程通讯一般都是 c 库内部处理的,干 python 啥事。上多机多卡,python io 和 c 库 io 就又没区别了
|
32
trustbutverify 2019-11-20 11:46:04 +08:00 via Android
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
|
33
ipwx 2019-11-20 11:46:37 +08:00 via Android 1
所以总结一下,python 世界的 c 库比你想象的多,表现形式也比你想象的多。去看看 numba,基本原理是通过 jit 把 Python 代码编译成本机代码。
|
34
msg7086 2019-11-20 11:52:04 +08:00 1
数据共享和多线程并行本来就是冲突的概念。
多线程下多个线程会同时访问同一块内存、同一堆对象,所以必然要对这些区域加锁,加锁就导致线程无法并行。 多进程可以满血并行,但是就会出现你说的进程之间通信开销过高的问题。 换句话说,8 个核心访问同一个变量会 GIL,8 个核心访问 8 个单独的变量副本会带来通信开销。 |
35
wuwukai007 2019-11-20 11:57:17 +08:00 via Android
走网络或者数据库的,瓶颈不在 cpu,开线程蛮快的
|
36
ryd994 2019-11-20 12:03:10 +08:00 via Android
Gil 是 CPython 才有的。你用其他 Python runtime 不一定有
|
38
jdhao 2019-11-20 12:28:46 +08:00 via Android
多线程和并行不是一个概念,一个 cpu 一个核也可以多线程
|
39
superrichman 2019-11-20 12:31:31 +08:00 via iPhone
有 gil 在,多线程效率上不去,要最大化利用资源还是搞 asyncio 加多进程。最近迷上了 asyncio,啥都想用异步来写。
|
40
BingoXuan 2019-11-20 14:19:07 +08:00
python 的 thread 是标准的 posix 线程,你不能因为不能用多核心就说人家不是多线程。超线程技术实际也是通过充分利用 cpu 资源来模拟多个线程而已。
如果你要做到并行计算且数据不共用情况下,直接开多个子进程运行任务就好了。如果你要共用数据进行并行计算,还是用专门计算库,如 cuda。 |
41
lewinlan 2019-11-20 15:30:06 +08:00
刚入门的时候也被坑了,甚至一度认为“一个核只能运行一个进程”,笑哭。
学学操作系统,再学个 Golang,你会知道除了线程还有协程在等着你。 至于楼上说用其他解释器的,我认为是纯粹炫技。Python 终究是个脚本语言,别太认真 |
42
rubycedar 2019-11-20 15:35:18 +08:00 via iPhone
村通网?...
|
43
daimiaopeng 2019-11-20 16:02:40 +08:00 via Android
多线程还是调用 boost python 自己做的 c++线程舒服
|
44
neoblackcap 2019-11-20 16:38:25 +08:00
@daimiaopeng 用 pybind11 吧
|
45
lolizeppelin 2019-11-20 16:40:32 +08:00 1
不要把逻辑纠结到名词上,名词是为了表意而已,代码本身脱离不了人类逻辑
34 楼说了关键,能不能并发(这个词本身就不应该纠结),需要逻辑上可行! 单关键点不是在 34 楼所说的共享,关键点在于,多个 work/thread/process/runner/loop (用哪个名词真不重要)之间的工作是否有序 你要知道一旦执行过程的逻辑是有序的, 那么最终无法一起做或者只能片段一起做(部分无序部分可以同时进行),即使你分配到了多个线程,最终顺序还是要用锁来保证,结果还是强制了顺序,即使看上去用了多个 cpu,实际都浪费到锁上 python 的线程是真线程, 但是 GIL 要求 python 代码片段被顺序执行,所以多个线程并没有同时干活,也就是没法用多核,最多跑 100%. 你的 python 代码是跑 python 虚拟机里的,GIL 就相当于 python 虚拟机里有个强制的线程顺序锁,你纯 python 代码怎么写都没法脱离他的顺序限制,自然是无法达成你需要的并发的 所以要么写 C 代码脱离 python 虚拟机的 GIL 限制,要么多进程 |
46
zappos 2019-11-20 17:17:42 +08:00 via Android
单核上的线程本来就没办法做到并行。
多核上的可以,但你要考虑 python 诞生的时候牙膏厂还没开始堆核。在那个时候有没有 gil 是看不出来区别的。 都是历史的眼泪。 |
47
kevinmissu 2019-11-20 18:09:15 +08:00
怪不得 每次面试都会问多线程多进程,大家理解都不一样 ,看到理解一样的 就觉得这个小伙子不错跟我理解一样,就招了, 要是不一样,不好意思 回去等通知~~~~~~娱乐一下 哈哈哈
|
49
Leigg 2019-11-20 19:20:56 +08:00 via Android
是并发,只是单核并发,在多 io 任务是有效果的。
|
50
secondwtq 2019-11-20 20:03:55 +08:00 1
@lolizeppelin 怎么不应该纠结名词 ... 特定的名词在特定的上下文里有特定的意思,相近不等于相等。语言背后的含义当然是最重要的,但是不准确的语言无法准确表达意思
这个帖子的大多数内容其实都是由语言的问题引发的,从楼主的标题开始,就没有能准确地传达意思。其实大家(可能要排除发这贴之前的楼主 ...)都知道楼主标题要表达什么意思,毕竟是 Python 一个 well-known 的坑,但是大家也都在挑楼主”不是真正的多线程“语句上的毛病 ... 以及楼主说的”真正并发还是要靠多进程“ ... 从楼主的回复看楼主是明白一些(英文)名词的,只不过 map 到中文不大会,以及可能发帖的时候没太注意 这一切都恰好说明了把话从一开始说准,说对是非常重要的 我预感此楼可能要 @FrankHB ... |
51
lolizeppelin 2019-11-20 20:29:13 +08:00
@secondwtq
相近但是不常用的词没法准确传递的 最明显的就是并行和并发, 这两词具体表达什么意思是“定义”下来而不是可直接理解推导的 今天我看了记得定义了明天就忘记了, 那些本来就不熟悉这些内容的更加记不住了 所以用词得绕着表达, 比如利用多核这种可以直接理解的 |
52
clino 2019-11-20 20:34:37 +08:00 via Android
go 的 coroutine 都能用多核,这个应该是很大的优势
其他语言的 coroutine 应该都没有吧? |
53
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 |
54
Balthild 2019-11-21 16:31:27 +08:00
@clino 这个要看具体的调度器怎么设计。比如 Rust 里面,你使用不同的 executor,最终结果是在单核还是多核上跑也是不一样的。
|
55
FrankHB 2019-11-23 00:43:59 +08:00 1
所以这些 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 可以有多个物理核,如同一个抽象机进程中支持多个执行线程,这似乎符合最朴素的抽象。 但这和并行或者多任务根本上都没有实现以上的关系,因为如最开始提到的,“物理同时”从来就不是考虑这些抽象时必备的要素,仅仅是方便理解而提到罢了。 而且,实际的处理器内部也不可能完全重叠地利用计算时间,最显然地,要求时钟同步来避免非预期的不确定性。 此外,单一核仍然能通过异步中断能实现物理上同时的多任务,这和多线程也没有直接的关系。 |