Go 併發可視化解釋 - sync-Mute
在學習 Go 編程語言時,您可能會遇到這句著名的格言:“不要通過共享內存來進行通信;相反,通過通信來共享內存。” 這句話構成了 Go 強大併發模型的基礎,其中通道(channels)作爲協程之間的主要通信工具。然而,雖然通道是管理併發的多功能工具,但錯誤地假設我們應該始終用通道替換傳統的鎖定機制,如 Mutex,是一個錯誤的觀念。在某些情況下,使用 Mutex 不僅恰當,而且比通道更有效。
在我的 Go 併發可視化系列中,今天我將通過視覺方式來解釋 sync.Mutex
。
Golang 基礎
場景
想象一下,有四位 Gopher 自行車手每天騎車上班。他們都需要在到達辦公室後洗個澡,但辦公室只有一個浴室。爲了防止混亂,他們確保一次只能有一個人使用浴室。這種獨佔式訪問的概念正是 Go Mutex(互斥鎖)的核心。
每天早上在辦公室洗澡對自行車手和跑步者來說是一個小小的競爭。
普通模式
今天最早到達的是 Stringer。當他來的時候,沒有人在使用浴室,因此他可以立即使用浴室。
對一個未加鎖的 Mutex 調用 Lock() 會立即成功。
片刻後,Partier 到了。Partier 發現有人在使用浴室,但他不知道是誰,也不知道什麼時候會結束使用。此時,他有兩個選擇:站在浴室前面(主動等待),或者離開並稍後再回來(被動等待)。按 Go 的術語,前者被稱爲 “自旋”(spinning)。自旋的協程會佔用 CPU 資源,增加了在鎖定可用時獲取 Mutex 的機會,而無需進行昂貴的上下文切換。然而,如果 Mutex 不太可能很快可用,繼續佔用 CPU 資源會降低其他協程獲取 CPU 時間的機會。
從版本 1.21 開始,Golang 允許到達的協程自旋一段時間。如果在指定時間內無法獲取 Mutex,它將進入休眠狀態,以便其他協程有機會運行。
到達的協程首先自旋,然後休眠。
Candier 到了。就像 Partier 一樣,她試圖獲取浴室。
因爲她剛到,如果 Stringer 很快釋放浴室,她就有很大的機會在被動等待之前獲取它。這被稱爲普通模式。
普通模式的性能要好得多,因爲協程可以連續多次獲取 Mutex,即使有阻塞的等待者。
go/src/sync/mutex.go at go1.21.0 · golang/go · GitHub[1]
新到達的協程在爭奪所有權時具有優勢
飢餓模式
Partier 回來了。由於他等待的時間很長(超過 1 毫秒),他將嘗試以飢餓模式獲取浴室。當 Swimmer 來時,他注意到有人餓了,他不會嘗試獲取浴室,也不會自旋。相反,他會排隊在等待隊列的尾部。
在這種飢餓模式下,當 Candier 結束時,她會直接把浴室交給 Partier。此時沒有競爭。
飢餓模式是防止尾延遲的病理情況的重要措施。
Partier 完成了他的回合並釋放了浴室。此時,只有 Swimmer 在等待,因此他將立即擁有它。Swimmer 如果發現自己是最後一個等待的人,他會將 Mutex 設置回普通模式。如果他發現自己的等待時間少於 1 毫秒,也會這樣做。
最後,Swimmer 在使用浴室後釋放了它。請注意,Mutex 不會將所有者從 “已鎖定(由 Goroutine A 鎖定)” 狀態更改爲 “已鎖定(由 Goroutine B 鎖定)” 狀態。它始終會在 “已鎖定” 到“未鎖定”然後再到 “已鎖定” 的狀態之間切換。出於簡潔起見,上面的圖像中省略了中間狀態。
展示代碼!
Mutex 的實現隨時間而變化,實際上,要完全理解它的實現並不容易。幸運的是,我們不必完全理解其實現就能高效使用它。如果從這篇博客中只能記住一件事,那一定是:早到的人不一定贏得比賽。相反,新到達的協程通常具有更高的機會,因爲它們仍在 CPU 上運行。Golang 還嘗試避免通過實現飢餓模式來使等待者被餓死。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(4)
bathroom := sync.Mutex{}
takeAShower := func(name string) {
defer wg.Done()
fmt.Printf("%s: I want to take a shower. I'm trying to acquire the bathroom\n", name)
bathroom.Lock()
fmt.Printf("%s: I have the bathroom now, taking a shower\n", name)
time.Sleep(500 * time.Microsecond)
fmt.Printf("%s: I'm done, I'm unlocking the bathroom\n", name)
bathroom.Unlock()
}
go takeAShower("Partier")
go takeAShower("Candier")
go takeAShower("Stringer")
go takeAShower("Swimmer")
wg.Wait()
fmt.Println("main: Everyone is Done. Shutting down...")
}
正如您可能猜到的,併發代碼的結果幾乎總是非確定性的。
第一次
Swimmer: I want to take a shower. I'm trying to acquire the bathroom
Partier: I want to take a shower. I'm trying to acquire the bathroom
Candier: I want to take a shower. I'm trying to acquire the bathroom
Stringer: I want to take a shower. I'm trying to acquire the bathroom
Swimmer: I have the bathroom now, taking a shower
Swimmer: I'm done, I'm unlocking the bathroom
Partier: I have the bathroom now, taking a shower
Partier: I'm done, I'm unlocking the bathroom
Candier: I have the bathroom now, taking a shower
Candier: I'm done, I'm unlocking the bathroom
Stringer: I have the bathroom now, taking a shower
Stringer: I'm done, I'm unlocking the bathroom
main: Everyone is Done. Shutting down...
第二次
Swimmer: I want to take a shower. I'm trying to acquire the bathroom
Swimmer: I have the bathroom now, taking a shower
Partier: I want to take a shower. I'm trying to acquire the bathroom
Stringer: I want to take a shower. I'm trying to acquire the bathroom
Candier: I want to take a shower. I'm trying to acquire the bathroom
Swimmer: I'm done, I'm unlocking the bathroom
Partier: I have the bathroom now, taking a shower
Partier: I'm done, I'm unlocking the bathroom
Stringer: I have the bathroom now, taking a shower
Stringer: I'm done, I'm unlocking the bathroom
Candier: I have the bathroom now, taking a shower
Candier: I'm done, I'm unlocking the bathroom
main: Everyone is Done. Shutting down...
自己實現 Mutex
實現 sync.Mutex
是困難的,但使用具有緩衝的通道來實現 Mutex 卻相當容易。
type MyMutex struct {
ch chan bool
}
func NewMyMutex() *MyMutex {
return &MyMutex{
// 緩衝大小必須爲 1
ch: make(chan bool, 1),
}
}
// Lock 鎖定 m。
// 如果鎖已被使用,調用的協程將被阻塞,直到 Mutex 可用。
func (m *MyMutex) Lock() {
[m.ch](http://m.ch) <- true
}
// Unlock 解鎖 m。
func (m *MyMutex) Unlock() {
<-m.ch
}
這篇文章通過生動的場景和可視化效果很好地解釋了 Go 語言中 sync.Mutex
的工作原理,以及如何使用互斥鎖來管理併發
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ijKFz3lYC3SRuSDqKTp1lw