python2与python3中的多线程

5fjcxozz  于 2023-05-23  发布在  Python
关注(0)|答案(1)|浏览(199)

我熟悉Python的GIL,所以我知道多线程在Python中并不是真正的多线程。
当我运行下面的代码时,我期望结果是0,因为GIL不允许存在竞争条件。在python3中,结果是0。但在python 2中,它不是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)
wqsoz72f

wqsoz72f1#

语句x += 1在任何版本的Python中都不是线程安全的。事实上,你在Python 2中看到了竞争条件的结果,而在Python 3中没有看到,这主要是一个巧合(这可能与GIL在线程之间切换时的优化有关,但我不知道细节)。在Python 3中也可能得到错误的结果。
原因是+=操作符不是原子的。它需要几个字节码来运行,GIL只保证在任何一个字节码运行时防止线程之间的切换。让我们来看看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只确保解释器的内部状态保持正常。它不能保证你的代码的每一行都是原子的。

相关问题