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

问题描述 投票:24回答:5

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

当线程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的释放与T1重新获得之间存在竞争条件。如果T1首先尝试重新获取,则在T2的release()完成之前,它将被不必要地重新悬浮。

注意:我故意不使用with语句,以更好地用显式调用来说明竞争。

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

python multithreading python-3.x race-condition condition-variable
5个回答
4
投票

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

首先,Python的qazxsw poi。 Java qazxsw poi文档内容如下:

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

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

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

  1. 从服务员threading implementation is based on Java'ss锁定的那一刻起,即在Condition.signal()上释放它之前 - 它保证会被通知信号。如果相应的acquire()发生在信令之前,这将允许序列(其中P =生产者和C =消费者)wait(),在这种情况下,对应于相同流的release()P: release(); C: acquire(); P: notify(); C: wait()将错过信号。有些情况下这无关紧要(甚至可以认为更准确),但有些情况下这是不可取的。这是一个论点。
  2. 当你在锁外面使用wait()时,这可能导致调度优先级倒置;也就是说,低优先级线程最终可能优先于高优先级线程。考虑具有一个生产者和两个消费者的工作队列(LC =低优先级消费者和HC =高优先级消费者),其中LC当前正在执行工作项并且HC在acquire()中被阻止。

可能会出现以下顺序:

notify()

然而,如果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(); 之前,那么在HC被唤醒之前,LC将无法使用notify()。这是优先级倒置发生的地方。这是第二个论点。

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

Python's release() Module

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

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

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


2
投票

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

1. The notifier needs to take a lock

假装threading存在。

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

Condition

这失败是因为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 CPython确实唤醒了通知的线程,让它切换到等待互斥锁(而不仅仅是notify())。

2. The condition variable itself needs a lock

(as you observed)具有内部数据,在并发等待/通知的情况下必须对其进行保护。 (看一眼moving it to that wait queue,我看到两个不同步的Conditions可能会错误地针对同一个等待线程,这可能会导致吞吐量降低甚至死锁。)当然,它可以使用专用锁来保护数据;由于我们已经需要用户可见的锁,因此使用该锁可以避免额外的同步成本。

3. Multiple wake conditions can need the lock

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

the CPython implementation

假设notify()def setSignal(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() 并且线程#1在box.val等待。线程#2调用False;当它释放waitFor(box,True,cv)时,#1仍然被阻止了。线程#3然后调用setSignal,发现cvwaitFor(box,False,cv),并等待。然后#2调用box.val,醒来#3,这仍然不满意并再次阻止。现在#1和#3都在等待,尽管其中一个必须满足其条件。

True

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

4. The hardware might need a lock

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

5. Real-time systems might need it

在POSIX文本notify()之后我们立即wait()

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

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


0
投票

发生的事情是T1等待并释放锁定,然后T2获取它,通知cv唤醒T1。

不完全的。 find调用不会唤醒T1线程:它只会将其移动到不同的队列。在One blog post之前,T1正在等待条件成立。在cv.notify()之后,T1正在等待获得锁定。 T2没有释放锁,T1没有“唤醒”,直到T2显式调用notify()


0
投票

几个月前,我遇到了完全相同的问题。但是因为我打开了notify(),看着cv.release()的结果(这个方法的ipython)并没有花很长时间自己回答。

简而言之,threading.Condition.wait??方法创建了另一个名为waiter的锁,获取它,将其附加到列表中,然后惊讶地释放锁本身。之后,它再次获得了服务员,即它开始等待有人释放服务员。然后它再次获取锁定并返回。

source方法从服务员列表中弹出服务员(服务员是锁,我们记得)并释放它,允许相应的wait方法继续。

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

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

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


-2
投票

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

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

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

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


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

wait

和:

notify

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

© www.soinside.com 2019 - 2024. All rights reserved.