首页 文章

为什么Python threading.Condition()notify()需要锁定?

提问于
浏览
21

由于不必要的性能影响,我的问题特别提到为什么它是这样设计的 .

当线程T1有这个代码时:

cv.acquire()
cv.wait()
cv.release()

和线程T2有这个代码:

cv.acquire()
cv.notify()  # requires that lock be held
cv.release()

发生的事情是T1等待并释放锁定,然后T2获取它,通知 cv 唤醒T1 . 现在,从 wait() 返回后,在T2 's release and T1' s之间存在竞争条件 . 如果T1首先尝试重新获取,则会在T2的 release() 完成之前不必要地重新暂停 .

Note: 我故意不使用 with 语句,以更好地说明显式调用的竞争 .

这似乎是一个设计缺陷 . 有没有任何已知的理由,或者我错过了什么?

5 回答

  • -2

    这不是一个明确的答案,但它应该涵盖我设法收集的有关此问题的相关详细信息 .

    首先,Python的threading implementation is based on Java's . Java的 Condition.signal() 文档内容如下:

    当调用此方法时,实现可能(通常确实)要求当前线程保持与此Condition关联的锁 .

    现在,问题是为什么在Python中特别强制执行此行为 . 但首先我要介绍每种方法的优缺点 .

    至于为什么有人认为握住锁通常是一个更好的主意,我发现了两个主要论点:

    • 从服务员 acquire() 锁定的那一刻起,即在释放它之前 wait() -it保证被通知信号 . 如果相应的 release() 发生在信令之前,这将允许序列(其中P = 生产环境 者和C =消费者) P: release(); C: acquire(); P: notify(); C: wait() ,在这种情况下,对应于相同流的 acquire()wait() 将错过该信号 . 有些情况下这不是不合需要的 . 这是一个论点 .

    • 当您在锁定之外时,这可能会导致调度优先级倒置;也就是说,低优先级线程最终可能优先于高优先级线程 . 考虑具有一个 生产环境 者和两个消费者的工作队列(LC =低优先级消费者和HC =高优先级消费者),其中LC当前正在执行工作项并且HC在 wait() 中被阻止 .

    可能会出现以下顺序:

    P                    LC                    HC
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                         execute(item)                   (in wait())
    lock()                                  
    wq.push(item)
    release()
                         acquire()
                         item = wq.pop()
                         release();
    notify()
                                                         (wake-up)
                                                         while (wq.empty())
                                                           wait();
    

    如果 notify() 发生在 release() 之前,那么在HC被唤醒之前,LC将无法实现 acquire() . 这是优先级倒置发生的地方 . 这是第二个论点 .

    支持在锁之外进行通知的论点是针对高性能线程,其中线程不需要再次进入休眠状态,只是为了在下一次获取切片时再次唤醒 - 这已经解释了它是如何发生的 . 我的问题 .

    Python的线程模块

    在Python中,正如我所说,你必须在通知时握住锁 . 具有讽刺意味的是,内部实现不允许底层操作系统避免优先级倒置,因为它会对服务员执行FIFO命令 . 当然,服务员的顺序是确定性的这一事实可以派上用场,但问题仍然是为什么强制执行这样的事情,因为可以认为区分锁和条件变量会更加精确,因为一些流程需要优化的并发性和最小的阻塞, acquire() 本身不应该注册先前的等待状态,而只是 wait() 调用本身 .

    可以说,无论如何,Python程序员并不关心这种程度的性能 - 尽管仍然没有回答为什么在实现标准库时不应该允许几种标准行为的问题 .

    还有一点需要说明的是,由于某些原因, threading 模块的开发人员可能特别想要一个FIFO订单,并发现这是实现它的最佳方式,并且希望以牺牲费用的方式将其 Build 为 Condition 其他(可能更普遍)的方法 . 为此,他们应该得到怀疑的好处,直到他们自己解释它为止 .

  • 3

    T1会等待并释放锁定,然后T2获取它,通知cv唤醒T1 .

    不完全的 . cv.notify() 调用不会唤醒T1线程:它只会将其移动到不同的队列 . 在 notify() 之前,T1正在等待条件为真 . 在 notify() 之后,T1正在等待获取锁定 . T2不释放锁定,T1直到T2才会释放锁定显式调用 cv.release() .

  • 0

    几个月前,我遇到了完全相同的问题 . 但是自从我打开 ipython 后,查看 threading.Condition.wait?? 结果(方法的source)并没有花很长时间自己回答 .

    简而言之, wait 方法创建另一个名为waiter的锁,获取它,将其附加到列表然后,出乎意料,释放锁定 . 之后,它再次获得了服务员,即它开始等待有人释放服务员 . 然后它再次获取锁定并返回 .

    notify 方法从服务器列表中弹出一个服务员(服务员是一个锁,正如我们记得的那样)并释放它,允许相应的 wait 方法继续 .

    这就是诀窍是 wait 方法在等待 notify 方法释放服务员时没有对条件本身进行锁定 .

    UPD1 :我似乎误解了这个问题 . 在T2释放之前,T1可能会尝试重新获取锁定,这是否正确?

    但是在python的GIL环境中是否可能?或者您认为在释放条件之前可以插入IO调用,这将允许T1唤醒并永远等待?

  • 0

    有几个原因引人注目(合在一起) .

    1.通知程序需要锁定

    假装 Condition.notifyUnlocked() 存在 .

    标准的 生产环境 者/消费者安排要求双方锁定:

    def unlocked(qu,cv):  # qu is a thread-safe queue
      qu.push(make_stuff())
      cv.notifyUnlocked()
    def consume(qu,cv):
      with cv:
        while True:       # vs. other consumers or spurious wakeups
          if qu: break
          cv.wait()
        x=qu.pop()
      use_stuff(x)
    

    这会失败,因为 push()notifyUnlocked() 都可以介入 if qu:wait() 之间 .

    写任何一个

    def lockedNotify(qu,cv):
      qu.push(make_stuff())
      with cv: cv.notify()
    def lockedPush(qu,cv):
      x=make_stuff()      # don't hold the lock here
      with cv: qu.push(x)
      cv.notifyUnlocked()
    

    作品(这是一个有趣的演示) . 第二种形式的优点是删除了 qu 是线程安全的要求,但是它也不再需要锁定它来调用 notify() .

    仍然需要解释这样做的偏好,特别是考虑到CPython确实唤醒了通知的线程,让它切换到等待互斥锁(而不仅仅是moving it to that wait queue) .

    2.条件变量本身需要锁定

    Condition 具有内部数据,在并发等待/通知的情况下必须对其进行保护 . (看了一眼the CPython implementation,我看到两个未同步的 notify() 可能会错误地定位同一个等待线程,这可能会导致吞吐量降低甚至死锁 . )当然,它可以使用专用锁保护这些数据;由于我们已经需要用户可见的锁,因此使用该锁可以避免额外的同步成本 .

    3.多个唤醒条件可能需要锁定

    (改编自下面链接的博客文章评论 . )

    def setTrue(box,cv):
      signal=False
      with cv:
        if not box.val:
          box.val=True
          signal=True
      if signal: cv.notifyUnlocked()
    def waitFor(box,v,cv):
      v=bool(v)   # to use ==
      while True:
        with cv:
          if box.val==v: break
          cv.wait()
    

    假设 box.valFalse 并且线程#1正在 waitFor(box,True,cv) 中等待 . 线程#2调用 setSignal ;当它发布 cv 时,#1仍然被阻止 . 线程#3然后调用 waitFor(box,False,cv) ,发现 box.valTrue ,并等待 . 然后#2调用 notify() ,醒来#3,这仍然不满意并再次阻止 . 现在#1和#3都在等待,尽管其中一个必须满足其条件 .

    def setTrue(box,cv):
      with cv:
        if not box.val:
          box.val=True
          cv.notify()
    

    现在情况不会出现:#3在更新之前到达并且从不等待,或者它在更新期间或之后到达并且还没有等待,保证通知转到#1,从 waitFor 返回 .

    4.硬件可能需要锁定

    随着等待变形而没有GIL(在Python的一些替代或未来实现中), notify() 之后的锁定释放所施加的内存排序(参见Java's rules)和从 wait() 返回的锁定获取可能是通知的唯一保证 . 线程的更新对等待线程可见 .

    5.实时系统可能需要它

    在POSIX文本you quoted之后我们立即find

    但是,如果需要可预测的调度行为,则该互斥锁应由调用pthread_cond_broadcast()或pthread_cond_signal()的线程锁定 .

    One blog post进一步讨论了该建议的基本原理和历史(以及此处的一些其他问题) .

  • 0

    没有竞争条件,这是条件变量的工作原理 .

    调用wait()时,将释放基础锁,直到发生通知 . 保证等待的调用者将在函数返回之前重新获取锁(例如,在等待完成之后) .

    你是如果在调用notify()时直接唤醒T1,那么可能会有一些低效率 . 但是,条件变量通常是通过OS原语实现的,并且OS通常足够智能以实现T2仍然具有锁定,因此它不会立即唤醒T1而是将其排队以唤醒 .

    另外,在python中,这并不重要,因为由于GIL只有一个线程,所以线程无论如何都无法并发运行 .


    此外,最好使用以下表单,而不是直接调用acquire / release:

    with cv:
        cv.wait()
    

    和:

    with cv:
        cv.notify()
    

    这可确保即使发生异常也会释放底层锁 .

相关问题