【问题标题】:Why does this Python code with threading have race conditions?为什么这个带有线程的 Python 代码有竞争条件?
【发布时间】:2021-12-27 08:55:35
【问题描述】:

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

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]}')

输出:

$ 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

Python3 版本:

Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux

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

那么,GIL 究竟做了什么,线程什么时候并行运行?

【问题讨论】:

  • 这不应该是锁的工作吗?我的意思是为什么在添加列表编号时不使用锁?对列表的操作不保证原子性。您是否尝试过使用锁并发生同样的情况?另外我认为使用双端队列应该比简单的列表确保更多的原子性。有一个 * 答案,Alex martelli 指出,在使用线程时你应该结合使用双端队列和队列
  • @FedericoBaù 当然,当您知道这将发生时。当我十年前学习 Python 并尝试多线程时,它似乎应该像 Javascript 一样,执行整个函数直到它结束并让事件循环继续。
  • 用 Python 3.10 试试吧,我认为最近有一个有趣的问答指出这种情况不会再发生(尽管假设它不会发生并不安全)。

标签: python python-3.x gil


【解决方案1】:

阅读the docs更好,我认为有答案:

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

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

我不知道内部原理,但猜想这个字节码的每一行或每一块都是单独执行的,其他线程正在等待(这使它变慢)。但有些行由多个块组成,不是原子的。

如果运行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

其中一些以并发方式执行,并产生竞争条件。所以 GIL 只保证 listdict 等结构的内部不会损坏。

【讨论】:

  • 使用dis.dis("x[0] += 1")
最近更新 更多