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