由于不必要的性能影响,我的问题特别提到为什么它是这样设计的 .
当线程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 回答
这不是一个明确的答案,但它应该涵盖我设法收集的有关此问题的相关详细信息 .
首先,Python的threading implementation is based on Java's . Java的
Condition.signal()
文档内容如下:现在,问题是为什么在Python中特别强制执行此行为 . 但首先我要介绍每种方法的优缺点 .
至于为什么有人认为握住锁通常是一个更好的主意,我发现了两个主要论点:
从服务员
acquire()
锁定的那一刻起,即在释放它之前wait()
-it保证被通知信号 . 如果相应的release()
发生在信令之前,这将允许序列(其中P = 生产环境 者和C =消费者)P: release(); C: acquire(); P: notify(); C: wait()
,在这种情况下,对应于相同流的acquire()
的wait()
将错过该信号 . 有些情况下这不是不合需要的 . 这是一个论点 .当您在锁定之外时,这可能会导致调度优先级倒置;也就是说,低优先级线程最终可能优先于高优先级线程 . 考虑具有一个 生产环境 者和两个消费者的工作队列(LC =低优先级消费者和HC =高优先级消费者),其中LC当前正在执行工作项并且HC在
wait()
中被阻止 .可能会出现以下顺序:
如果
notify()
发生在release()
之前,那么在HC被唤醒之前,LC将无法实现acquire()
. 这是优先级倒置发生的地方 . 这是第二个论点 .支持在锁之外进行通知的论点是针对高性能线程,其中线程不需要再次进入休眠状态,只是为了在下一次获取切片时再次唤醒 - 这已经解释了它是如何发生的 . 我的问题 .
Python的线程模块
在Python中,正如我所说,你必须在通知时握住锁 . 具有讽刺意味的是,内部实现不允许底层操作系统避免优先级倒置,因为它会对服务员执行FIFO命令 . 当然,服务员的顺序是确定性的这一事实可以派上用场,但问题仍然是为什么强制执行这样的事情,因为可以认为区分锁和条件变量会更加精确,因为一些流程需要优化的并发性和最小的阻塞,
acquire()
本身不应该注册先前的等待状态,而只是wait()
调用本身 .可以说,无论如何,Python程序员并不关心这种程度的性能 - 尽管仍然没有回答为什么在实现标准库时不应该允许几种标准行为的问题 .
还有一点需要说明的是,由于某些原因,
threading
模块的开发人员可能特别想要一个FIFO订单,并发现这是实现它的最佳方式,并且希望以牺牲费用的方式将其 Build 为Condition
其他(可能更普遍)的方法 . 为此,他们应该得到怀疑的好处,直到他们自己解释它为止 .不完全的 .
cv.notify()
调用不会唤醒T1线程:它只会将其移动到不同的队列 . 在notify()
之前,T1正在等待条件为真 . 在notify()
之后,T1正在等待获取锁定 . T2不释放锁定,T1直到T2才会释放锁定显式调用cv.release()
.几个月前,我遇到了完全相同的问题 . 但是自从我打开
ipython
后,查看threading.Condition.wait??
结果(方法的source)并没有花很长时间自己回答 .简而言之,
wait
方法创建另一个名为waiter的锁,获取它,将其附加到列表然后,出乎意料,释放锁定 . 之后,它再次获得了服务员,即它开始等待有人释放服务员 . 然后它再次获取锁定并返回 .notify
方法从服务器列表中弹出一个服务员(服务员是一个锁,正如我们记得的那样)并释放它,允许相应的wait
方法继续 .这就是诀窍是
wait
方法在等待notify
方法释放服务员时没有对条件本身进行锁定 .UPD1 :我似乎误解了这个问题 . 在T2释放之前,T1可能会尝试重新获取锁定,这是否正确?
但是在python的GIL环境中是否可能?或者您认为在释放条件之前可以插入IO调用,这将允许T1唤醒并永远等待?
有几个原因引人注目(合在一起) .
1.通知程序需要锁定
假装
Condition.notifyUnlocked()
存在 .标准的 生产环境 者/消费者安排要求双方锁定:
这会失败,因为
push()
和notifyUnlocked()
都可以介入if qu:
和wait()
之间 .写任何一个
作品(这是一个有趣的演示) . 第二种形式的优点是删除了
qu
是线程安全的要求,但是它也不再需要锁定它来调用notify()
.仍然需要解释这样做的偏好,特别是考虑到CPython确实唤醒了通知的线程,让它切换到等待互斥锁(而不仅仅是moving it to that wait queue) .
2.条件变量本身需要锁定
Condition
具有内部数据,在并发等待/通知的情况下必须对其进行保护 . (看了一眼the CPython implementation,我看到两个未同步的notify()
可能会错误地定位同一个等待线程,这可能会导致吞吐量降低甚至死锁 . )当然,它可以使用专用锁保护这些数据;由于我们已经需要用户可见的锁,因此使用该锁可以避免额外的同步成本 .3.多个唤醒条件可能需要锁定
(改编自下面链接的博客文章评论 . )
假设
box.val
是False
并且线程#1正在waitFor(box,True,cv)
中等待 . 线程#2调用setSignal
;当它发布cv
时,#1仍然被阻止 . 线程#3然后调用waitFor(box,False,cv)
,发现box.val
是True
,并等待 . 然后#2调用notify()
,醒来#3,这仍然不满意并再次阻止 . 现在#1和#3都在等待,尽管其中一个必须满足其条件 .现在情况不会出现:#3在更新之前到达并且从不等待,或者它在更新期间或之后到达并且还没有等待,保证通知转到#1,从
waitFor
返回 .4.硬件可能需要锁定
随着等待变形而没有GIL(在Python的一些替代或未来实现中),
notify()
之后的锁定释放所施加的内存排序(参见Java's rules)和从wait()
返回的锁定获取可能是通知的唯一保证 . 线程的更新对等待线程可见 .5.实时系统可能需要它
在POSIX文本you quoted之后我们立即find:
One blog post进一步讨论了该建议的基本原理和历史(以及此处的一些其他问题) .
没有竞争条件,这是条件变量的工作原理 .
调用wait()时,将释放基础锁,直到发生通知 . 保证等待的调用者将在函数返回之前重新获取锁(例如,在等待完成之后) .
你是如果在调用notify()时直接唤醒T1,那么可能会有一些低效率 . 但是,条件变量通常是通过OS原语实现的,并且OS通常足够智能以实现T2仍然具有锁定,因此它不会立即唤醒T1而是将其排队以唤醒 .
另外,在python中,这并不重要,因为由于GIL只有一个线程,所以线程无论如何都无法并发运行 .
此外,最好使用以下表单,而不是直接调用acquire / release:
和:
这可确保即使发生异常也会释放底层锁 .