![](/img/trans.png)
[英]Why does Python's threading.Condition use an RLock by default?
[英]Why does Python threading.Condition() notify() require a lock?
我的問題特別指的是為什么它是這樣設計的,由於不必要的性能影響。
當線程 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 的線程實現是基於 Java 的. Java 的Condition.signal()
文檔內容如下:
當調用此方法時,實現可能(並且通常確實)要求當前線程持有與此 Condition 關聯的鎖。
現在,問題是為什么要特別在 Python 中強制執行這種行為。 但首先我想介紹每種方法的優缺點。
至於為什么有些人認為持有鎖通常是一個更好的主意,我發現了兩個主要論點:
從wait()
acquire()
鎖定的那一刻開始 - 即在wait()
釋放它之前 - 保證會收到信號通知。 如果相應的release()
在發信號之前發生,這將允許序列(其中P=Producer和C=Consumer ) P: release(); C: acquire(); P: notify(); C: wait()
P: release(); C: acquire(); P: notify(); C: wait()
P: release(); C: acquire(); P: notify(); C: wait()
在這種情況下,與同一流的acquire()
對應的wait()
將錯過信號。 有些情況下這無關緊要(甚至可以被認為更准確),但有些情況下這是不可取的。 這是一種說法。
當你在鎖外notify()
,這可能會導致調度優先級倒置; 也就是說,低優先級線程最終可能會優先於高優先級線程。 考慮一個有一個生產者和兩個消費者( LC=Low-priority consumer和HC=High-priority consumer )的工作隊列,其中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()
之前, LC將無法在HC被喚醒之前acquire()
。 這就是發生優先級反轉的地方。 這是第二個論點。
支持在鎖外進行通知的論點是針對高性能線程,在這種情況下,線程不需要回到睡眠狀態,只是為了在它獲得的下一個時間片再次喚醒——這已經解釋了它是如何發生的我的問題。
threading
模塊在 Python 中,正如我所說,您必須在通知時持有鎖。 具有諷刺意味的是,內部實現不允許底層操作系統避免優先級反轉,因為它對等待程序強制執行 FIFO 順序。 當然,服務員的順序是確定性的這一事實可能會派上用場,但問題仍然是為什么要強制執行這樣的事情,因為有人會爭辯說區分鎖和條件變量會更精確,因為在一些需要優化並發和最小阻塞的流, acquire()
不應該自己注冊前面的等待狀態,而應該只注冊wait()
調用本身。
可以說,Python 程序員無論如何都不會關心性能到這種程度——盡管這仍然沒有回答為什么在實現標准庫時,一個人不應該允許多個標准行為成為可能的問題。
還有一件事要說的是, threading
模塊的開發人員可能出於某種原因特別想要一個 FIFO 順序,並發現這是實現它的最佳方式,並希望以犧牲為代價將其建立為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
是線程安全的,但它的成本沒有更多的鎖把它隨時待命,以notify()
為好。
仍然需要解釋這樣做的偏好,特別是考慮到(正如您所觀察到的) CPython 確實喚醒了被通知的線程以使其切換到等待互斥鎖(而不是簡單地將其移動到該等待隊列)。
Condition
具有在並發等待/通知的情況下必須受到保護的內部數據。 (看看CPython 的實現,我看到兩個未同步的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()
假設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 都在等待,盡管其中之一必須滿足其條件。
def setTrue(box,cv):
with cv:
if not box.val:
box.val=True
cv.notify()
現在不會出現這種情況:#3 在更新之前到達並且從不等待,或者它在更新期間或之后到達並且尚未等待,保證通知轉到 #1,后者從waitFor
返回。
使用等待變形且沒有 GIL(在 Python 的某些替代或未來實現中), notify()
之后的鎖釋放和從wait()
返回時的鎖獲取強加的內存排序(參見Java 的規則wait()
可能是僅保證通知線程的更新對等待線程可見。
但是,如果需要可預測的調度行為,則該互斥鎖應由調用 pthread_cond_broadcast() 或 pthread_cond_signal() 的線程鎖定。
一篇博文進一步討論了此建議的基本原理和歷史(以及此處的其他一些問題)。
發生的事情是 T1 等待並釋放鎖,然后 T2 獲取它,通知 cv 喚醒 T1。
不完全的。 cv.notify()
調用不會喚醒T1 線程:它只會將其移動到不同的隊列。 在notify()
之前,T1 正在等待條件為真。 在notify()
,T1 正在等待獲取鎖。 T2 不會釋放鎖,並且 T1 不會“喚醒”,直到 T2 顯式調用cv.release()
。
幾個月前,我遇到了完全相同的問題。 但是因為我打開了ipython
,看着threading.Condition.wait??
結果(該方法的來源)很快就自己回答了。
簡而言之, wait
方法創建另一個稱為 waiter 的鎖,獲取它,將它附加到一個列表中,然后出人意料地釋放對自身的鎖。 之后它再次獲取服務員,即它開始等待直到有人釋放服務員。 然后它再次獲取自身的鎖並返回。
notify
方法從waiter 列表中彈出一個waiter(waiter 是一個鎖,我們記得)並釋放它,允許相應的wait
方法繼續。
訣竅在於, wait
方法在等待notify
方法釋放waiter 時並沒有持有條件本身的鎖。
UPD1 :我似乎誤解了這個問題。 您是否擔心 T1 可能會在 T2 釋放之前嘗試重新獲取對自身的鎖定?
但是在python的GIL上下文中可能嗎? 或者你認為可以在釋放條件之前插入一個 IO 調用,這將允許 T1 喚醒並永遠等待?
它在 Python 3 文檔中進行了解釋: https : //docs.python.org/3/library/threading.html#condition-objects 。
注意:notify() 和 notify_all() 方法不會釋放鎖; 這意味着被喚醒的線程不會立即從它們的 wait() 調用中返回,而是只有在調用 notify() 或 notify_all() 的線程最終放棄鎖的所有權時才會返回。
沒有競爭條件,這就是條件變量的工作方式。
當wait()被調用時,底層的鎖被釋放,直到一個通知發生。 可以保證wait 的調用者在函數返回之前(例如,在wait 完成之后)重新獲取鎖。
如果在調用 notify() 時直接喚醒 T1,則可能會有些低效。 然而,條件變量通常是通過 OS 原語實現的,而且 OS 通常足夠聰明,可以意識到 T2 仍然擁有鎖,因此它不會立即喚醒 T1,而是將其排隊等待喚醒。
此外,在 python 中,這無論如何都無關緊要,因為由於 GIL 只有一個線程,所以線程無論如何都無法並發運行。
此外,最好使用以下形式而不是直接調用acquire/release:
with cv:
cv.wait()
和:
with cv:
cv.notify()
這確保即使發生異常也能釋放底層鎖。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.