我熟悉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)
1条答案
按热度按时间wqsoz72f1#
语句
x += 1
在任何版本的Python中都不是线程安全的。事实上,你在Python 2中看到了竞争条件的结果,而在Python 3中没有看到,这主要是一个巧合(这可能与GIL在线程之间切换时的优化有关,但我不知道细节)。在Python 3中也可能得到错误的结果。原因是
+=
操作符不是原子的。它需要几个字节码来运行,GIL只保证在任何一个字节码运行时防止线程之间的切换。让我们来看看foo
函数的反汇编,看看它是如何工作的(这是从Python 3.7开始的,在Python 2.7中,字节码中的地址是不同的,但所有的操作都是相同的):我们关心的是字节码位置为14-20的四行。前两个函数加载加法的参数。第三个执行
INPLACE_ADD
操作。加法的结果被放回堆栈,因为不是所有类型的对象都可以就地更新(整数不能,所以在这里是必要的)。最后一个字节码将总和存储回原始名称。如果解释器选择在我们在字节码14中加载
x
和在字节码20中再次存储新值之间切换哪个线程持有GIL,我们可能会得到一个不正确的结果,因为当我们再次获得GIL时,我们之前加载的值可能不再有效。正如我上面提到的,在Python 3中得到
0
的事实只是一个实现细节的结果,解释器在测试时选择不在字节码的关键部分切换。如果您在另一种情况下再次运行该程序,并不能保证它不会选择不同的方法(例如:在重CPU负载下),或者在不同的解释器版本中(例如,3.7而不是3.6,或者其他什么)。如果你想要真实的的线程安全,那么你应该使用实际的锁,而不是仅仅依赖于GIL。GIL只确保解释器的内部状态保持正常。它不能保证你的代码的每一行都是原子的。