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 只确保解释器的内部状态保持正常。它不能保证你的每一行代码都是原子的。
我熟悉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 只确保解释器的内部状态保持正常。它不能保证你的每一行代码都是原子的。