python 3.7 多线程中的 GIL 行为
GIL behavior in python 3.7 multithreading
我正在研究并试图理解 python GIL 和在 python 中使用多线程的最佳实践。我发现 this presentation and this video
我试图重现演示文稿前 4 张幻灯片中提到的奇怪和疯狂的问题。讲师在视频中也提到了这个问题(前4分钟)。
我写了这个简单的代码来重现问题
from threading import Thread
from time import time
BIG_NUMBER = 100000
count = BIG_NUMBER
def countdown(n):
global count
for i in range(n):
count -= 1
start = time()
countdown(count)
end = time()
print('Without Threading: Final count = {final_n}, Execution Time = {exec_time}'.format(final_n=count, exec_time=end - start))
count = BIG_NUMBER
a = Thread(target=countdown, args=(BIG_NUMBER//2,))
b = Thread(target=countdown, args=(BIG_NUMBER//2,))
start = time()
a.start()
b.start()
a.join()
b.join()
end = time()
print('With Threading: Final count = {final_n}, Execution Time = {exec_time}'.format(final_n=count, exec_time=end - start))
但是结果和论文视频完全不一样!
使用线程和不使用线程的执行时间几乎相同。有时两种情况中的一种比另一种快一点。
这是我在 Windows 10 下使用多核架构处理器使用 CPython 3.7.3 得到的结果。
Without Threading: Final count = 0, Execution Time = 0.02498459815979004
With Threading: Final count = 21, Execution Time = 0.023985862731933594
另外我根据视频和论文理解的是 GIL 阻止两个线程同时在两个核心中真正并行执行。所以如果这是真的,为什么最终计数变量(在多线程情况下)不是预期的零,并且在每次执行结束时会是一个不同的数字,可能是因为同时操作线程?
在比视频和论文(使用 python 3.2)更新的 python 中,GIL 是否发生了任何变化导致这些不同?
提前致谢
关于 Solomon 的评论,您编写的代码给出不一致结果的原因是 python 没有原子就地运算符。 GIL 确实保护 python 的内部结构不被混淆,但您的用户代码仍然必须保护自己。如果我们使用 dis
模块查看您的 countdown
函数,我们可以看到可能发生故障的位置。
>>> print(dis(countdown))
3 0 SETUP_LOOP 24 (to 26)
2 LOAD_GLOBAL 0 (range)
4 LOAD_FAST 0 (n)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 12 (to 24)
12 STORE_FAST 1 (i)
<strong>4 14 LOAD_GLOBAL 1 (count)
16 LOAD_CONST 1 (1)
18 INPLACE_SUBTRACT
20 STORE_GLOBAL 1 (count)</strong>
22 JUMP_ABSOLUTE 10
>> 24 POP_BLOCK
>> 26 LOAD_CONST 0 (None)
28 RETURN_VALUE
None
循环内的减法运算实际上需要4条指令才能完成。如果线程在 14 LOAD_GLOBAL 1 (count)
之后但行 20 STORE_GLOBAL 1 (count)
之前被中断,则其他线程可能会进入并修改 count
。然后,当执行返回到第一个线程时,count
的先前值用于减法,结果将覆盖另一个线程所做的任何修改。和 Solomon 一样,我不是 python 底层内部结构的专家,但我相信 GIL 确保字节码指令是原子的,但不会进一步。
Python不直接执行。它首先被编译成所谓的Python bytecode。该字节码在其思想上类似于原始汇编。字节码被执行。
GIL 的作用是不允许两个字节码指令并行 运行。虽然一些操作(例如io)确实在内部发布了GIL以允许真正的并发,当它可以证明它不会破坏任何东西时。
现在您只需要知道 count -= 1
不会 编译成单个字节码指令。它实际上编译成4条指令
LOAD_GLOBAL 1 (count)
LOAD_CONST 1 (1)
INPLACE_SUBTRACT
STORE_GLOBAL 1 (count)
大致意思是
load global variable into local variable
load 1 into local variable
subtract 1 from local variable
set global to the current local variable
这些指令中的每一个 都是 原子的。但是顺序可以被线程混合,这就是为什么你看到你所看到的。
那么 GIL 做了什么使得执行流程串行化。意味着指令一个接一个地发生,没有什么是平行的。因此,理论上,当您 运行 多个线程时,它们将执行与单线程相同的操作,减去在(所谓的)上下文切换上花费的一些时间。我在 Python3.6 中的测试确认执行时间相似。
但是在 Python2.7 中,我的测试显示线程的性能显着下降,大约 1.5 倍。我不知道这是为什么。除了 GIL 之外的其他事情必须在后台发生。
我正在研究并试图理解 python GIL 和在 python 中使用多线程的最佳实践。我发现 this presentation and this video
我试图重现演示文稿前 4 张幻灯片中提到的奇怪和疯狂的问题。讲师在视频中也提到了这个问题(前4分钟)。 我写了这个简单的代码来重现问题
from threading import Thread
from time import time
BIG_NUMBER = 100000
count = BIG_NUMBER
def countdown(n):
global count
for i in range(n):
count -= 1
start = time()
countdown(count)
end = time()
print('Without Threading: Final count = {final_n}, Execution Time = {exec_time}'.format(final_n=count, exec_time=end - start))
count = BIG_NUMBER
a = Thread(target=countdown, args=(BIG_NUMBER//2,))
b = Thread(target=countdown, args=(BIG_NUMBER//2,))
start = time()
a.start()
b.start()
a.join()
b.join()
end = time()
print('With Threading: Final count = {final_n}, Execution Time = {exec_time}'.format(final_n=count, exec_time=end - start))
但是结果和论文视频完全不一样! 使用线程和不使用线程的执行时间几乎相同。有时两种情况中的一种比另一种快一点。
这是我在 Windows 10 下使用多核架构处理器使用 CPython 3.7.3 得到的结果。
Without Threading: Final count = 0, Execution Time = 0.02498459815979004
With Threading: Final count = 21, Execution Time = 0.023985862731933594
另外我根据视频和论文理解的是 GIL 阻止两个线程同时在两个核心中真正并行执行。所以如果这是真的,为什么最终计数变量(在多线程情况下)不是预期的零,并且在每次执行结束时会是一个不同的数字,可能是因为同时操作线程? 在比视频和论文(使用 python 3.2)更新的 python 中,GIL 是否发生了任何变化导致这些不同? 提前致谢
关于 Solomon 的评论,您编写的代码给出不一致结果的原因是 python 没有原子就地运算符。 GIL 确实保护 python 的内部结构不被混淆,但您的用户代码仍然必须保护自己。如果我们使用 dis
模块查看您的 countdown
函数,我们可以看到可能发生故障的位置。
>>> print(dis(countdown))
3 0 SETUP_LOOP 24 (to 26)
2 LOAD_GLOBAL 0 (range)
4 LOAD_FAST 0 (n)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 12 (to 24)
12 STORE_FAST 1 (i)
<strong>4 14 LOAD_GLOBAL 1 (count)
16 LOAD_CONST 1 (1)
18 INPLACE_SUBTRACT
20 STORE_GLOBAL 1 (count)</strong>
22 JUMP_ABSOLUTE 10
>> 24 POP_BLOCK
>> 26 LOAD_CONST 0 (None)
28 RETURN_VALUE
None
循环内的减法运算实际上需要4条指令才能完成。如果线程在 14 LOAD_GLOBAL 1 (count)
之后但行 20 STORE_GLOBAL 1 (count)
之前被中断,则其他线程可能会进入并修改 count
。然后,当执行返回到第一个线程时,count
的先前值用于减法,结果将覆盖另一个线程所做的任何修改。和 Solomon 一样,我不是 python 底层内部结构的专家,但我相信 GIL 确保字节码指令是原子的,但不会进一步。
Python不直接执行。它首先被编译成所谓的Python bytecode。该字节码在其思想上类似于原始汇编。字节码被执行。
GIL 的作用是不允许两个字节码指令并行 运行。虽然一些操作(例如io)确实在内部发布了GIL以允许真正的并发,当它可以证明它不会破坏任何东西时。
现在您只需要知道 count -= 1
不会 编译成单个字节码指令。它实际上编译成4条指令
LOAD_GLOBAL 1 (count)
LOAD_CONST 1 (1)
INPLACE_SUBTRACT
STORE_GLOBAL 1 (count)
大致意思是
load global variable into local variable
load 1 into local variable
subtract 1 from local variable
set global to the current local variable
这些指令中的每一个 都是 原子的。但是顺序可以被线程混合,这就是为什么你看到你所看到的。
那么 GIL 做了什么使得执行流程串行化。意味着指令一个接一个地发生,没有什么是平行的。因此,理论上,当您 运行 多个线程时,它们将执行与单线程相同的操作,减去在(所谓的)上下文切换上花费的一些时间。我在 Python3.6 中的测试确认执行时间相似。
但是在 Python2.7 中,我的测试显示线程的性能显着下降,大约 1.5 倍。我不知道这是为什么。除了 GIL 之外的其他事情必须在后台发生。