python2 与 python 中的多线程 3

Multithreading in python2 vs python 3

我熟悉Python的GIL,所以我知道Python中的多线程并不是真正的多线程。

当我 运行 下面的代码时,我预计结果会是 0,因为 GIL 不允许竞争条件存在。在python3中,结果是0。但是在python2中,结果不是0;结果是一个意想不到的结果,例如 -3492 或 21283。

我该如何解决这个问题?

import threading 
x = 0 # A shared value

def foo(): 
  global x 
  for i in range(100000000): 
    x += 1 

def bar(): 
  global x 
  for i in range(100000000):
    x -= 1 

t1 = threading.Thread(target=foo) 
t2 = threading.Thread(target=bar) 

t1.start() 
t2.start() 

t1.join() 
t2.join() # Wait for completion

print(x)

语句 x += 1 在 Python 的任何版本中都不是线程安全的。您在 Python 2 而不是 Python 3 中看到了竞争条件的结果,这在很大程度上只是巧合(它可能与 GIL 在线程之间切换时的优化有关,但是我不知道细节)。它也可能在 Python 3 中得到错误的结果。

原因是 += 运算符不是原子的。它需要多个字节码到运行,而GIL只保证在任何一个字节码是运行ning时防止线程之间的切换。让我们看一下 foo 函数的反汇编,看看它是如何工作的(这是来自 Python 3.7,在 Python 2.7 中字节码中的地址不同,但所有操作都是相同):

>>> dis.dis(foo)
  3           0 SETUP_LOOP              24 (to 26)
              2 LOAD_GLOBAL              0 (range)
              4 LOAD_CONST               1 (100000000)
              6 CALL_FUNCTION            1
              8 GET_ITER
        >>   10 FOR_ITER                12 (to 24)
             12 STORE_FAST               0 (i)

  4          14 LOAD_GLOBAL              1 (x)
             16 LOAD_CONST               2 (1)
             18 INPLACE_ADD
             20 STORE_GLOBAL             1 (x)
             22 JUMP_ABSOLUTE           10
        >>   24 POP_BLOCK
        >>   26 LOAD_CONST               0 (None)
             28 RETURN_VALUE

我们关心的是字节码位置为 14-20 的四行。前两个将参数加载到加法中。第三个执行 INPLACE_ADD 操作。添加的结果被放回堆栈,因为并非所有类型的对象都可以就地更新(整数不能,所以这里有必要)。最后一个字节码将和存储回原始名称。

如果解释器选择在我们在字节码 14 中加载 x 和我们在字节码 20 中再次将新值存储到它之间时切换哪个线程持有 GIL,我们可能会以结果不正确,因为当我们再次获得 GIL 时,我们之前加载的值可能不再有效。

正如我上面提到的,你在 Python 3 中得到 0 的事实只是一个实现细节的结果,解释器选择在字节码的关键部分期间不切换你测试它的时间。如果您在另一种情况下(例如在重 CPU 负载下)或在不同的解释器版本(例如 3.7 而不是 3.6 或其他)中再次 运行 程序,则不能保证它不会做出不同的选择.

如果你想要真正的线程安全,那么你应该使用真正的锁,而不是仅仅依赖于 GIL。 GIL 只确保解释器的内部状态保持正常。它不能保证你的每一行代码都是原子的。