在 Go 中使用 Mutex 與 Channel 進行狀態同步
我通過谷歌搜尋查詢哪種方法最適合 GoLang 的同步工作。當我開始構建自己的 GoLang 包 Go-Log 時,我就開始了這項工作。 Go-Log 是一個日誌包,在 Go 的普通 Log 包之上提供實用程式,具有以下功能:將日誌標記為除錯和錯誤變體,向日志新增 / 刪除時間戳以及在日誌中獲取呼叫功能詳細資訊。這就非常需要使這個日誌記錄執行緒安全,或者需要同步,因為當數百萬個請求進入伺服器日誌時需要以較少的延遲同步。
通過瀏覽文章,StackOverflow 問答,以及常用的 Go 教學視訊時,我找到了兩種方法:
- 使用執行緒安全的 Channel
- 使用 Mutex 共享記憶體
Go 開發人員最常犯的錯誤是什麼?
在開始討論選擇哪種方法之前,所有人需要知道作為 Go 開發人員易犯的常見錯誤。我知道大家都醉心於併發模式,這種模式會讓你過度使用 Go 強力的 Channel 和 Goroutine,最終成為一種反模式。我並不是說 Channel 不好或者它們不能用於同步,但過度使用(在沒有要求的情況下使用它)絕對不是你應該遵循的途徑。
Dave Cheney 是 Go 程式語言的開源貢獻者和專案成員,曾在一次採訪中說過,“如果你想談論最糟糕的程式碼,那就是我曾試圖將 Channel 用於一切工作” Go 的官方文件指出,“常見的 Go 新手錯誤是過度使用 Channel 和 Goroutine,僅僅只是因為它是可能的,和/或因為它很有趣。”
現在假設您根本沒有關於 Goroutine 和 Channel 或 Mutex 的任何概念,讓我們看看之前提到的關於如何進行同步,兩種方法之間的區別。
使用通道進行通訊以實現同步
Channel 是連線併發 Goroutine 的管道,您可以從一個 Goroutine 將值傳送到 Channel,並在另一個 Goroutine 中接收值。Channel 最適合傳遞資料所有權,分發工作單元和傳遞非同步結果等情況。
讓我們利用 Channel 來實現 Golang 包的同步(目的是使日誌同步工作並保證執行緒安全),下面是我之前寫的包中的程式碼片段。
func generateMessage(message string) { // 建立布林型緩衝 Channel done := make(chan bool, 1) go printMessage(done, message) // 阻塞主程,否則 Goroutine 來不及執行主程式已結束 <-done//Channel 傳遞值並未使用 fmt.Println("main program end!") } func printMessage(done chan bool, message string) { // 延遲函式在 return 之前執行,否則向 Channel 傳送資料主程就結束 defer func() { // 向通道傳送資料 fmt.Println("chanel send data!") done <- true }() // 內部實現 if true { log.Println(message) return } fmt.Println(message) }
我們使用基本 Channel 通訊對多個 Goroutine 進行同步,這可能在伺服器記錄時發生數百萬次,關於通道的優點是它們具有內建的執行緒安全性並防止竟態條件。
如果你仔細看我的程式碼,並詢問是否明確需要 Channel ?答案是否定的。Channel 是 Go 中的高階概念,Go 中某些程式僅使用 Mutex, Go 的 Channel 很吸引人因為它們提供了內建的執行緒安全性,並鼓勵對共享的關鍵資源進行單執行緒訪問。 但是與 Mutex 相比,Channel 會導致效能下降。 當只需要鎖定少量共享資源時,使用 Mutex 非常有用。 如果 Mutex 很適合你的需求請放心使用 sync.Mutex。
我如何使用 Mutex 使日誌記錄同步?
Mutex 在跨多個 Goroutine 同步狀態時是一個極有用的資源,但我發現它的使用對於新的 Go 開發人員來說有些神祕。不過它很容易使用!這是我的軟體包 Go-Log 的程式碼片段
var mutex = &sync.Mutex{}// 互斥鎖 var wg sync.WaitGroup func generateMessage(message string) { wg.Add(1) // 增加 WaitGroup 計數 1 go func() { // 延遲函式,當 Goroutine 退出時減少 WaitGroup 計數 1 defer wg.Done() printMessage(message) }() wg.Wait() // 等待所有 Goroutine,當計數為 0 時等待結束 } func printMessage(resultMessage string) { mutex.Lock()// 互斥鎖鎖定 defer mutex.Unlock() // 延遲函式退出時解鎖 // 內部實現 : Ignore if true { log.Println(resultMessage) return } fmt.Println(resultMessage) }
程式中使用了 mutex.Lock() 和 mutex.Unlock() 來建立共享資源的同步鎖。為了管理多個日誌,每次都要建立一個 Goroutine 並將它們新增到 sync.WaitGroup,這是一個重要的同步原語,允許協作的 Goroutine 在再次獨立進行之前共同等待閾值事件。
在上面的例子中,這種方法更好更快! 我們減少了不必要的開銷。 但是如果您發現 sync.Mutex 鎖定規則過於複雜,捫心自問使用通道是否真的更簡單。
檢視我的軟體包的原始碼,瞭解我實際上是如何使用 Mutex 以實現同步:
https://github.com/MindorksOpenSource/Go-Log
附加資源
- [Closing] https://www.acloudtree.com/understanding-when-to-use-channels-or-mutexes-in-go/
- [MutexOrChannel] https://github.com/golang/go/wiki/MutexOrChannel
聯絡我
- Twitter: https://twitter.com/DuaYashish
- MentorCruise: https://mentorcruise.com/mentor/YashishDua/