首頁 > 後端開發 > Golang > Gosync.Cond,最被忽略的同步機制

Gosync.Cond,最被忽略的同步機制

DDD
發布: 2024-10-30 06:43:28
原創
325 人瀏覽過

這是帖子的摘錄;完整的帖子可以在這裡找到:https://victoriametrics.com/blog/go-sync-cond/

這篇文章是關於 Go 中處理並發的系列文章的一部分:

  • Gosync.Mutex:正常與飢餓模式
  • Gosync.WaitGroup 與對齊問題
  • Gosync.Pool 及其背後的機制
  • 使用sync.Cond,最被忽略的同步機制(我們來了)
  • Gosync.Map:適合正確工作的正確工具
  • Go Singleflight 融入您的程式碼,而不是您的資料庫

在Go中,sync.Cond是一個同步原語,儘管它不像sync.Mutex或sync.WaitGroup那麼常用。您很少會在大多數專案中甚至在標準庫中看到它,而其他同步機制往往會取代它。

也就是說,身為 Go 工程師,你不會真的希望自己在閱讀使用sync.Cond 的程式碼時卻不知道發生了什麼,因為畢竟它是標準函式庫的一部分。

因此,本次討論將幫助您縮小這一差距,更好的是,它會讓您更清楚地了解它在實踐中的實際運作方式。

什麼是sync.Cond?

那麼,讓我們來分析一下sync.Cond 的意義。

當 goroutine 需要等待特定事情發生時,例如某些共享資料更改,它可以“阻塞”,這意味著它只是暫停其工作,直到獲得繼續的許可。最基本的方法是使用循環,甚至可能添加一個 time.Sleep 來防止 CPU 因忙碌等待而瘋狂。

這可能是這樣的:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登入後複製
登入後複製
登入後複製
登入後複製

現在,這並不是真正有效,因為該循環仍在後台運行,消耗 CPU 週期,即使沒有任何更改。

這就是sync.Cond 發揮作用的地方,這是讓 goroutine 協調工作的更好方法。從技術上講,如果您來自更學術的背景,那麼它是一個「條件變數」。

  • 當一個goroutine正在等待某件事發生時(等待某個條件成立),它可以呼叫Wait()。
  • 另一個 Goroutine,一旦知道條件可能滿足,就可以呼叫 Signal() 或 Broadcast() 來喚醒等待的 Goroutine,並讓它們知道是時候繼續前進了。

這是sync.Cond的基本介面:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
登入後複製
登入後複製
登入後複製

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond 概述

好吧,讓我們來看一個快速的偽範例。這次,我們有一個 Pokémon 主題,假設我們正在等待一個特定的 Pokémon,並且我們希望在它出現時通知其他 Goroutines。

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登入後複製
登入後複製
登入後複製
登入後複製

在此範例中,一個 Goroutine 正在等待皮卡丘出現,而另一個 Goroutine(生產者)從清單中隨機選擇一個神奇寶貝,並在新神奇寶貝出現時向消費者發出信號。

當生產者發送訊號時,消費者醒來並檢查是否出現了正確的神奇寶貝。如果有,我們就捕獲神奇寶貝,如果沒有,消費者就回去睡覺並等待下一個。

問題是,生產者發送訊號和消費者實際醒來之間存在差距。同時,Pokémon 可能會發生變化,因為消費者 Goroutine 可能會晚於 1 毫秒(很少)醒來,或者其他 Goroutine 會修改共享的 Pokemon。所以sync.Cond 基本上是在說:'嘿,有些東西改了!醒來看看,但如果太晚了,可能又會改變了。 '

如果消費者起晚了,Pokémon 可能會逃跑,而 Goroutine 會重新進入睡眠狀態。

「嗯,我可以用一個通道來將 Pokemon 名稱或訊號傳送給另一個 Goroutine」

當然。事實上,在 Go 中,通道通常比sync.Cond更受歡迎,因為它們更簡單,更慣用,並且為大多數開發人員所熟悉。

在上面的情況下,您可以輕鬆地透過通道發送 Pokémon 名稱,或者僅使用空 struct{} 來發出訊號而不發送任何資料。但我們的問題不僅是透過通道傳遞訊息,還涉及處理共享狀態。

我們的例子非常簡單,但是如果多個 goroutine 存取共享的 pokemon 變量,讓我們看看如果我們使用通道會發生什麼:

  • 如果我們使用通道發送 Pokémon 名稱,我們仍然需要一個互斥體來保護共享的 pokemon 變數。
  • 如果我們僅使用通道來發出訊號,則仍然需要互斥體來管理對共享狀態的存取。
  • 如果我們在生產者中檢查皮卡丘,然後透過通道發送它,我們還需要一個互斥體。最重要的是,我們違反了關注點分離原則,即生產者承擔了真正屬於消費者的邏輯。

也就是說,當多個 goroutine 修改共享資料時,仍然需要互斥體來保護它。在這些情況下,您經常會看到通道和互斥體的組合,以確保正確的同步和資料安全。

「好的,但是廣播訊號呢?」

好問題!您確實可以透過簡單地關閉通道(close(ch))來使用通道向所有等待的 goroutine 模仿廣播訊號。當您關閉通道時,從該通道接收的所有 goroutine 都會收到通知。但請記住,關閉的通道無法重複使用,一旦關閉,它就會保持關閉。

順便說一句,實際上有人在談論 Go 2 中刪除sync.Cond:提案:sync:刪除 Cond 類型。

「那麼,sync.Cond 有什麼用呢?」

嗯,在某些情況下,sync.Cond 可能比通道更合適。

  1. 使用通道,你可以透過發送值的方式向一個 goroutine 發送訊號,也可以透過關閉通道來通知所有 goroutine,但你不能同時執行這兩種操作。 sync.Cond 為您提供更細粒度的控制。你可以呼叫 Signal() 來喚醒單一 goroutine,或呼叫 Broadcast() 來喚醒所有 goroutine。
  2. 並且您可以根據需要多次呼叫 Broadcast(),而通道一旦關閉就無法執行此操作(關閉已關閉的通道會引發恐慌)。
  3. 通道不提供保護共享資料的內建方法 - 您需要使用互斥體單獨管理它。另一方面,sync.Cond 透過將鎖定和訊號傳送到一個套件中,為您提供了一種更整合的方法(以及更好的效能)。

「為什麼要在sync.Cond嵌入Lock?」

理論上,像sync.Cond 這樣的條件變數不必綁定到鎖即可使其訊號正常運作。

您可以讓使用者在條件變數之外管理自己的鎖,這聽起來像是提供了更大的靈活性。這並不是真正的技術限制,而更多的是人為錯誤。

手動管理很容易導致錯誤,因為該模式不太直觀,您必須在呼叫 Wait() 之前解鎖互斥體,然後在 goroutine 喚醒時再次鎖定它。這個過程可能會讓人感到尷尬,而且很容易出錯,例如忘記在正確的時間鎖定或解鎖。

但是為什麼圖案看起來有點不對勁?

通常,呼叫 cond.Wait() 的 goroutine 需要在循環中檢查某些共享狀態,如下所示:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登入後複製
登入後複製
登入後複製
登入後複製

sync.Cond 中嵌入的鎖定幫助我們處理鎖定/解鎖過程,使程式碼更簡潔且不易出錯,我們很快就會詳細討論該模式。

如何使用?

如果仔細觀察前面的範例,您會注意到消費者中的一致模式:我們總是在等待(.Wait())條件之前鎖定互斥體,並在滿足條件後解鎖它。

另外,我們將等待條件包裝在一個循環中,這裡複習一下:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
登入後複製
登入後複製
登入後複製

條件等待()

當我們在sync.Cond 上呼叫Wait() 時,我們是在告訴目前的goroutine 堅持下去,直到滿足某些條件。

這是幕後發生的事情:

  1. 該 goroutine 被加入到其他也在等待相同條件的 goroutine 清單中。所有這些 goroutine 都被阻塞,這意味著它們無法繼續,直到被 Signal() 或 Broadcast() 呼叫「喚醒」。
  2. 這裡的關鍵部分是,在呼叫 Wait() 之前必須鎖定互斥體,因為 Wait() 做了一些重要的事情,它會在讓 goroutine 休眠之前自動釋放鎖(呼叫 Unlock())。這允許其他 Goroutine 在原始 Goroutine 等待時獲取鎖定並完成其工作。
  3. 當等待的 goroutine 被喚醒(透過 Signal() 或 Broadcast())時,它不會立即恢復工作。首先,它必須重新取得鎖(Lock())。

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond.Wait() 方法

以下是 Wait() 在底層的工作原理:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登入後複製
登入後複製
登入後複製
登入後複製

雖然很簡單,但我們可以總結4個重點:

  1. 有一個檢查器可以防止複製 Cond 實例,如果這樣做會出現恐慌。
  2. 呼叫 cond.Wait() 會立即解鎖互斥體,因此在呼叫 cond.Wait() 之前必須鎖定互斥體,否則會出現恐慌。
  3. 被喚醒後,cond.Wait() 會重新鎖定互斥體,這表示您在使用完共享資料後需要再次解鎖它。
  4. sync.Cond 的大部分功能是在 Go 運行時中透過名為 notificationList 的內部資料結構實現的,該結構使用基於票據的系統進行通知。

由於這種鎖定/解鎖行為,在使用sync.Cond.Wait() 時您將遵循一個典型模式以避免常見錯誤:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
登入後複製
登入後複製
登入後複製

Go sync.Cond, the Most Overlooked Sync Mechanism

使用sync.Cond.Wait()的典型模式

「為什麼不直接用 c.Wait() 而不使用循環呢?」


這是帖子的摘錄;完整的帖子可以在這裡找到:https://victoriametrics.com/blog/go-sync-cond/

以上是Gosync.Cond,最被忽略的同步機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板