为什么这个带有线程的 Python 代码有竞争条件?

cul*_*rón 7 python gil python-3.x

此代码创建了一个竞争条件:

import threading

ITERS = 100000
x = [0]

def worker():
    for _ in range(ITERS):
        x[0] += 1  # this line creates a race condition
        # because it takes a value, increments and then writes
        # some inrcements can be done together, and lost

def main():
    x[0] = 0  # you may use `global x` instead of this list trick too
    t1 = threading.Thread(target=worker)
    t2 = threading.Thread(target=worker)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

for i in range(5):
    main()
    print(f'iteration {i}. expected x = {ITERS*2}, got {x[0]}')
Run Code Online (Sandbox Code Playgroud)

输出:

$ python3 test.py
iteration 0. expected x = 200000, got 200000
iteration 1. expected x = 200000, got 148115
iteration 2. expected x = 200000, got 155071
iteration 3. expected x = 200000, got 200000
iteration 4. expected x = 200000, got 200000
Run Code Online (Sandbox Code Playgroud)

Python3版本:

Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux
Run Code Online (Sandbox Code Playgroud)

我认为 GIL 会阻止它,并且不允许两个线程一起运行,直到它们执行与 io 相关的操作或调用 C 库。至少这是您可以从文档中得出的结论。

事实证明我错了。那么,GIL 实际上是做什么的,线程何时并行运行?

cul*_*rón 2

更好地阅读文档,我认为答案是:

CPython 解释器用来确保一次只有一个线程执行 Python 字节码的机制。这通过使对象模型(包括关键内置类型,例如 dict)针对并发访问隐式安全,简化了 CPython 实现。锁定整个解释器使解释器更容易成为多线程,但代价是多处理器机器提供的大部分并行性。

然而,一些扩展模块(无论是标准的还是第三方的)被设计为在执行压缩或散列等计算密集型任务时释放 GIL。而且,在进行 I/O 时 GIL 总是被释放。

我猜这意味着每行源代码都由多个字节码块组成。字节码行/块是原子的,即它们单独执行,但源代码行不是。

这是扩展为的字节代码+=1(运行dis.dis('x[0] += 1')查看):

          0 LOAD_NAME                0 (x)
          2 LOAD_CONST               0 (0)
          4 DUP_TOP_TWO
          6 BINARY_SUBSCR
          8 LOAD_CONST               1 (1)
         10 INPLACE_ADD
         12 ROT_THREE
         14 STORE_SUBSCR
         16 LOAD_CONST               2 (None)
         18 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

当这些行以并发方式执行时,就会出现竞争条件。

所以,GIL 并不能拯救你。它只能防止可能损坏复杂结构(如list或 )的竞争条件dict