我正在研究并试图理解python GIL以及在python中使用多线程的最佳实践。我找到了this presentation和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在两个核心同时防止两个线程真正并行执行。所以,如果这是真的,为什么最终计数变量(在多线程情况下)不是预期的零,并且在每次执行结束时将是一个不同的数字可能是因为同时操作线程?更新的蟒蛇中的GIL发生了什么变化而不是视频和纸张(使用python 3.2)导致这些不同?提前致谢
Python不是直接执行的。它首先被编译成所谓的Python bytecode。这个字节码的思想与原始程序集类似。字节码被执行。
GIL的作用是不允许两个字节码指令并行运行。虽然有些opeartions(例如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的内部结构不被混淆,但是你的用户代码仍然必须保护自己。如果我们使用countdown
模块查看您的dis
函数,我们可以看到故障发生的位置。
>>> 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)
4 14 LOAD_GLOBAL 1 (count)
16 LOAD_CONST 1 (1)
18 INPLACE_SUBTRACT
20 STORE_GLOBAL 1 (count)
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确保字节码指令是原子的,但不是更进一步。