簡體   English   中英

為什么 Python threading.Condition() notify() 需要鎖?

[英]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 中強制執行這種行為。 但首先我想介紹每種方法的優缺點。

至於為什么有些人認為持有鎖通常是一個更好的主意,我發現了兩個主要論點:

  1. wait() acquire()鎖定的那一刻開始 - 即在wait()釋放它之前 - 保證會收到信號通知。 如果相應的release()在發信號之前發生,這將允許序列(其中P=ProducerC=ConsumerP: 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()將錯過信號。 有些情況下這無關緊要(甚至可以被認為更准確),但有些情況下這是不可取的。 這是一種說法。

  2. 當你在鎖外notify() ,這可能會導致調度優先級倒置; 也就是說,低優先級線程最終可能會優先於高優先級線程。 考慮一個有一個生產者和兩個消費者( LC=Low-priority consumerHC=High-priority consumer )的工作隊列,其中LC當前正在執行一個工作項,而HCwait()被阻塞。

可能會出現以下順序:

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() 這就是發生優先級反轉的地方。 這是第二個論點。

支持在鎖外進行通知的論點是針對高性能線程,在這種情況下,線程不需要回到睡眠狀態,只是為了在它獲得的下一個時間片再次喚醒——這已經解釋了它是如何發生的我的問題。

Python 的threading模塊

在 Python 中,正如我所說,您必須在通知時持有鎖。 具有諷刺意味的是,內部實現不允許底層操作系統避免優先級反轉,因為它對等待程序強制執行 FIFO 順序。 當然,服務員的順序是確定性的這一事實可能會派上用場,但問題仍然是為什么要強制執行這樣的事情,因為有人會爭辯說區分鎖和條件變量會更精確,因為在一些需要優化並發和最小阻塞的流, acquire()不應該自己注冊前面的等待狀態,而應該只注冊wait()調用本身。

可以說,Python 程序員無論如何都不會關心性能到這種程度——盡管這仍然沒有回答為什么在實現標准庫時,一個人不應該允許多個標准行為成為可能的問題。

還有一件事要說的是, threading模塊的開發人員可能出於某種原因特別想要一個 FIFO 順序,並發現這是實現它的最佳方式,並希望以犧牲為代價將其建立為Condition其他(可能更普遍)的方法。 為此,他們值得懷疑,直到他們自己解釋。

有幾個原因令人信服(綜合考慮)。

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 確實喚醒了被通知的線程以使其切換到等待互斥鎖(而不是簡單地將其移動到該等待隊列)。

2.條件變量本身需要鎖

Condition具有在並發等待/通知的情況下必須受到保護的內部數據。 (看看CPython 的實現,我看到兩個未同步的notify()可能錯誤地指向同一個等待線程,這可能導致吞吐量降低甚至死鎖。)當然,它可以用專用鎖保護數據; 因為我們已經需要一個用戶可見的鎖,使用它可以避免額外的同步成本。

3. 多個喚醒條件可能需要鎖

(改編自對下面鏈接的博客文章的評論。)

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.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()之后的鎖釋放和從wait()返回時的鎖獲取強加的內存排序(參見Java 的規則wait()可能是僅保證通知線程的更新對等待線程可見。

5. 實時系統可能需要它

您引用的 POSIX 文本之后,我們立即發現

但是,如果需要可預測的調度行為,則該互斥鎖應由調用 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM