這是帖子的摘錄;完整的帖子可以在這裡找到:https://victoriametrics.com/blog/go-sync-cond/
這篇文章是關於 Go 中處理並發的系列文章的一部分:
在Go中,sync.Cond是一個同步原語,儘管它不像sync.Mutex或sync.WaitGroup那麼常用。您很少會在大多數專案中甚至在標準庫中看到它,而其他同步機制往往會取代它。
也就是說,身為 Go 工程師,你不會真的希望自己在閱讀使用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 協調工作的更好方法。從技術上講,如果您來自更學術的背景,那麼它是一個「條件變數」。
這是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() {}
好吧,讓我們來看一個快速的偽範例。這次,我們有一個 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 變量,讓我們看看如果我們使用通道會發生什麼:
也就是說,當多個 goroutine 修改共享資料時,仍然需要互斥體來保護它。在這些情況下,您經常會看到通道和互斥體的組合,以確保正確的同步和資料安全。
「好的,但是廣播訊號呢?」
好問題!您確實可以透過簡單地關閉通道(close(ch))來使用通道向所有等待的 goroutine 模仿廣播訊號。當您關閉通道時,從該通道接收的所有 goroutine 都會收到通知。但請記住,關閉的通道無法重複使用,一旦關閉,它就會保持關閉。
順便說一句,實際上有人在談論 Go 2 中刪除sync.Cond:提案:sync:刪除 Cond 類型。
「那麼,sync.Cond 有什麼用呢?」
嗯,在某些情況下,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 堅持下去,直到滿足某些條件。
這是幕後發生的事情:
以下是 Wait() 在底層的工作原理:
// wait until condition is true for !condition { } // or for !condition { time.Sleep(100 * time.Millisecond) }
雖然很簡單,但我們可以總結4個重點:
由於這種鎖定/解鎖行為,在使用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() {}
「為什麼不直接用 c.Wait() 而不使用循環呢?」
這是帖子的摘錄;完整的帖子可以在這裡找到:https://victoriametrics.com/blog/go-sync-cond/
以上是Gosync.Cond,最被忽略的同步機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!