RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1606949
Accepted
AsLimbo
AsLimbo
Asked:2025-02-15 14:59:40 +0000 UTC2025-02-15 14:59:40 +0000 UTC 2025-02-15 14:59:40 +0000 UTC

Python 的 += 增量对竞争条件的影响

  • 772

尝试在Python的不同变体中重现竞争条件。以下是一个例子:

import threading, time
count = 0

def counter():
    global count
    c = count # 100 потоков одновременно изменили переменную c, присвоив ей значение count + 1, то есть 0+1 = 1
    time.sleep(0.1) # потоки заснули
    count = c + 1 # Потоки проснулись и просто присвоили переменной count значение 0+1 100 раз.
    print(count) # печать count после повторного присвоения единицы.

for t in range(100):
    thr = threading.Thread(target=counter)
    thr.start()

(修改)
这里100个线程进入函数,给变量counter赋值,然后睡眠0.1秒。在睡眠状态下,线程会释放GIL,这就是它们随后尝试同时增加的原因。然而,问题不在于线程捕获相同的状态。事实上,在给定的例子中,变量 count 不能以任何方式改变,因为变量始终等于 1,并且每个线程进入第 135 行,只是将2 分配给该变量,这是始终的事实的结果。这里没有竞争条件——GIL 成功阻止多个线程获取锁。我们得到以下输出:countccount = c + 1count = 0ccountcount = 2

#...
# 1
# 1
# 1
# 1
# 1
# 1
# 1
# 1

然后我尝试稍微改变一下这个功能:

import threading, time
count = 0

def counter():
    global count
    time.sleep(0.1)
    count = count + 1
    print(count)

for t in range(100):
    thr = threading.Thread(target=counter)
    thr.start()

与前一种情况相比,这里唯一的区别是,我们不是创建中间变量,而是count用其自身增加它。除此之外,一切都一样——100 个线程进入该函数,休眠 0.1 秒,这会导致GIL被释放,并且变量会增加count = count + 1。但是,竞争条件并没有发生!结论:

#...
# 94
# 95
# 96
# 97
# 98
# 99
# 100

变量count等于100,为什么会出现这种情况?看起来增量操作 += 是原子的。也就是说,可以假设在这个例子中,由于字节码级别操作的原子性,GIL 不允许多个线程同时工作。

python-3.x
  • 2 2 个回答
  • 154 Views

2 个回答

  • Voted
  1. Best Answer
    Stanislav Volodarskiy
    2025-02-16T03:14:11Z2025-02-16T03:14:11Z

    代码count = count + 1以原子方式执行。代码count += 1也是。但这件作品已不复存在:

        def term():
            return 1
    
        count += term()
    

    该语言本身不提供任何代码原子性的保证。但是在您的 CPython 版本中,有些选项是原子执行的。这取决于解释器及其版本。例如,在 CPython 3.9 中没有原子性。并且在 3.10 和 3.11 她出现了。我有 CPython 3.11,我会在上面展示它。

    注意:不要依赖原子性。它没有任何保证并且可能随时被违反。他们会在解释器中发现一个错误,修复它,然后原子性就会消失。没必要冒险。

    Python 将源代码编译为字节码,然后进行解释。count = count + 1被编译成以下的指令序列(这是一个反汇编程序,代码本身当然是二进制的,易于解释):

      8           2 LOAD_GLOBAL              0 (count)
                 14 LOAD_CONST               1 (1)
                 16 BINARY_OP                0 (+)
                 20 STORE_GLOBAL             0 (count)
    

    堆栈用于处理数据。操作顺序:
    将一个值放入堆栈count,
    将一个值放在顶部,
    调用加法指令,
    将结果保存在中count。

    解释器_PyEval_EvalFrameDefault循环读取指令并执行它们。

    处理说明LOAD_CONST:

            TARGET(LOAD_CONST) {
                PREDICTED(LOAD_CONST);
                PyObject *value = GETITEM(consts, oparg);
                Py_INCREF(value);
                PUSH(value);
                DISPATCH();
            }
    

    这里有很多宏,但其含义并没有丢失:我们从一些常量存储中获取一个值并将其放在堆栈上。请注意挑战DISPATCH();。该宏隐藏了goto将处理下一条指令的标签。

    假设一个进程有两个线程正在运行,那么 Python 会从其中一个线程执行一些代码,从另一个线程执行一些代码。线程永远不会并行执行,GIL负责这一点。在 Python 中无法中断线程的执行。相反,解释器会定期检查是否有其他线程正在等待访问处理器。如果处于待处理状态,解释器将停止当前线程中的解释,释放 GIL,并将当前线程排队等待执行。

    切换过于频繁会浪费时间,Python 每 5 毫秒切换线程不会超过一次。可以通过调用 进行调整sys.setswitchinterval。

    也就是说,当一个线程获得控制权时,它会定期检查标志eval_breaker。五毫秒后该标志将被升起,解释器将注意到这一点并放弃控制。控制总是在指令之间进行。

    翻译员多久检查一次eval_breaker?不经常。标志检查通过调用完成CHECK_EVAL_BREAKER();。再看一下处理代码LOAD_CONST。那里不存在这样的挑战。解释器将无条件地继续执行下一条指令。此后,LOAD_CONST电流流动就无法中断。

    如果您还检查LOAD_GLOBAL,BINARY_OP和STORE_GLOBAL,那么所有这些都无法验证。这段代码作为单个原子指令执行。

    那么为什么带有函数调用的代码会中断呢?

        def term():
            return 1
    
        count += term()
    

    因为它包含一个函数调用:

     12           8 LOAD_GLOBAL              0 (count)
                 20 PUSH_NULL
                 22 LOAD_FAST                0 (term)
                 24 PRECALL                  0
                 28 CALL                     0
                 38 BINARY_OP                0 (+)
                 42 STORE_GLOBAL             0 (count)
    

    CALL执行标志检查:

            TARGET(CALL) {
                ...
                CHECK_EVAL_BREAKER();
                DISPATCH();
            }
    

    您的第一个代码中断不是因为您调用了time.sleep(...),而是因为您在读取全局变量的值并将其写回之间调用了某个函数。

    数据竞赛的秘诀:

    • 流必须运行超过五毫秒;
    • 在读取和写入之间必须有一个指令,之后解释器准备放弃控制(调用函数是最简单的选择)。
    import threading
    
    m = 1_000_000
    count = 0
    
    
    def counter():
        global count
    
        def one():
            return 1
    
        for _ in range(m):
            count += one()
    
    
    threads = [threading.Thread(target=counter) for _ in range(2)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    print(count, len(threads) * m)
    
    $ python --version
    Python 3.11.2
    
    $ python race.py
    1634669 2000000
    

    PS练习:想出一个代码,从所有其他线程中夺走控制权至少一秒钟。

    • 5
  2. Serge3leo
    2025-02-16T17:25:45Z2025-02-16T17:25:45Z

    尝试在 Python 的不同变体中重现竞争条件。

    对于为什么没有成功,答案如下:

    1. 为了进行“竞赛”,需要多个活跃的参与方;
    2. 需要时间重叠。

    如果您采取许多短流,其中大多数将根本不活跃。此外,在大多数情况下,如果您不插入time.sleep(),则前一个线程将在下一个线程开始之前结束。

    需要更加谨慎地为“种族”的出现创造条件,例如:

    import threading
    
    def Fib(n):
        return 0 if not n else 1 if 1 == n else Fib(n - 1) + Fib(n - 2)
    
    class test_thread:
        def __init__(self, nthrs, nops, func=None, args=None):
            self.counter = 0
            self.nops = nops
            self.func = func
            self.args = args
            self.thrs = [threading.Thread(target=self.body) 
                         for i in range(nthrs)]
        def calc(self):
            if self.func:
                return self.func(*self.args)
            return 1
        def body(self):
            if self.func:
                for i in range(self.nops):
                    self.counter += self.func(*self.args)
            else:
                for i in range(self.nops):
                    self.counter += 1
        def test(self):
            start = self.counter
            for thr in self.thrs:
                thr.start()
            for thr in self.thrs:
                thr.join()
            finish = self.counter
            diff = (finish - start) - len(self.thrs)*self.nops*self.calc()
            status = "FAIL" if diff else "OK"
            print(f"{status}, func={self.func}, args={self.args} "
                  f"diff={diff} start={start} finish={finish}")
    
    x = test_thread(8, 100_000)
    x.test()
    x = test_thread(8, 100_000, Fib, [7])
    x.test()
    

    很容易看出,对于传统的 CPython(带有 GIL),竞争条件很可能出现在以下形式的语句中counter += Fib(7):

    $ python3.13 /tmp/test_thread.py
    OK, func=None, args=None diff=0 start=0 finish=800000
    FAIL, func=<function Fib at 0x10f23b560>, args=[7] diff=-8523476 start=0 finish=1876524
    

    然而,即使在一些具有 GIL 的版本上,也相对容易看出该操作符也是counter += 1非原子的:

    $ python3.8 /tmp/test_thread.py 
    FAIL, func=None, args=None diff=-127286 start=0 finish=672714
    FAIL, func=<function Fib at 0x10ad361f0>, args=[7] diff=-8939567 start=0 finish=1460433
    

    嗯,特别是对于自由线程 CPython(没有 GIL),它们很可能同时发生在counter += Fib(7)和上counter += 1:

    $ python3.13t /tmp/test_thread.py 
    FAIL, func=None, args=None diff=-656289 start=0 finish=143711
    FAIL, func=<function Fib at 0x3d5d45808c0>, args=[7] diff=-9081085 start=0 finish=1318915
    

    有关详细信息,您应该阅读PEP 583 – Python 的并发内存模型,因为 CPython 2.7 实现似乎严格保证不间断性counter += 1。在 3.x 中,没有这样的保证,调度程序有时会使过程复杂化。

    附言

    看起来增量操作 += 是原子的。也就是说,可以假设在这个例子中,由于字节码级别操作的原子性,

    不值得一提的是,在字节码中,这些甚至是几种操作,而不是一种特殊的操作,因此在任何情况下,都会有数据发生不可预测的变化。也许在调试器中,也许在使用共享内存时multiprocessing.shared_memory,也许在其他情况下。唯一的问题是这种事件发生的概率。

    • 2

相关问题

  • 在 Linux 服务器上运行 Django 项目

  • 当您单击kivy设置中的关闭按钮时,如何调用更新应用程序本身的gui的方法

  • 制作一个按钮处理程序来调用该函数。那些。单击按钮时,该函数应运行。遥控机器人

  • 如何正确地将列表项添加到 Word 表格中?

  • 内容解析(Python、BeautifulSoup、请求)

  • 脚本不适用于 BeautifulSoup 和请求 (Python3x)

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    我看不懂措辞

    • 1 个回答
  • Marko Smith

    请求的模块“del”不提供名为“default”的导出

    • 3 个回答
  • Marko Smith

    "!+tab" 在 HTML 的 vs 代码中不起作用

    • 5 个回答
  • Marko Smith

    我正在尝试解决“猜词”的问题。Python

    • 2 个回答
  • Marko Smith

    可以使用哪些命令将当前指针移动到指定的提交而不更改工作目录中的文件?

    • 1 个回答
  • Marko Smith

    Python解析野莓

    • 1 个回答
  • Marko Smith

    问题:“警告:检查最新版本的 pip 时出错。”

    • 2 个回答
  • Marko Smith

    帮助编写一个用值填充变量的循环。解决这个问题

    • 2 个回答
  • Marko Smith

    尽管依赖数组为空,但在渲染上调用了 2 次 useEffect

    • 2 个回答
  • Marko Smith

    数据不通过 Telegram.WebApp.sendData 发送

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5