01
介紹
Go 標準庫 sync
提供互斥鎖 Mutex
。它的零值是未鎖定的 Mutex
,即未被任何 goroutine
所持有,它在被首次使用後,不可以複製。
我們可以使用 Mutex
限定同一時間只允許一個 goroutine
訪問和修改臨界區。
02
使用
在介紹怎麼使用 Mutex
之前,我們先閱讀 `sync.Mutex` 原始碼[1]:
// [the Go memory model]: https://go.dev/ref/mem type Mutex struct { state int32 sema uint32 } func (m *Mutex) Lock() { // ... } func (m *Mutex) TryLock() bool { // ... } func (m *Mutex) Unlock() { // ... }
閱讀原始碼,我們可以發現,Mutex
提供了三個方法,分別是 Lock
、Unlock
和 Go 1.18 新增的 TryLock
。
我們可以使用 Mutex
的 Lock
方法獲取鎖,獲取鎖的 goroutine
可以訪問和修改臨界區,此時其它 goroutine
如果也想要訪問和修改臨界區,則會被阻塞,等待當前獲取鎖的 goroutine
釋放鎖。
持有鎖的 goroutine
釋放鎖,可以使用 Mutex
的 Unlock
方法。
細心的讀者朋友們,可能已經發現,Go 1.18 新增的 TryLock
是三個方法中唯一有返回值的方法,因為 TryLock
方法可以透過 bool
返回值通知當前準備爭搶鎖的 goroutine
是否搶到鎖,該 goroutine
可以根據返回值決定做什麼,而不僅是被阻塞,還可以自由選擇做其它事情。
推薦讀者朋友們閱讀 the Go memory model[2],更加深入瞭解 Mutex
。
使用方式
單變數
func main() { // 定義變數 mu var mu sync.Mutex go func() { mu.Lock() fmt.Println("g1 get lock") time.Sleep(time.Second * 10) mu.Unlock() }() time.Sleep(time.Second * 5) // main goroutine if mu.TryLock() { fmt.Println("main get lock") mu.Unlock() } else { // main goroutine not get lock, do other thing fmt.Println("do other things first") } }
struct 欄位
type Counter struct { mu sync.Mutex count map[string]int } func main() { c := &Counter{ count: make(map[string]int), } go func() { c.mu.Lock() c.count["lucy"] = 1 fmt.Println("g1 goroutine:",c.count) time.Sleep(time.Second * 5) c.mu.Unlock() }() time.Sleep(time.Second * 2) // main goroutine c.count["lucy"] = 10 fmt.Println("main goroutine:",c.count) }
閱讀程式碼,細心的讀者朋友們可能已經發現,不管是單變數,還是作為 struct
中的欄位,我們都未初始化 sync.Mutex
型別的變數,而是直接使用它的零值。
當然,初始化也可以。
03
陷阱
想要用好 Mutex
,我們還需要注意一些“陷阱”。
陷阱一
Go 語言中的互斥鎖 Mutex
,即使一個 goroutine
未持有鎖,它也可以執行 Unlock
釋放鎖。
如果一個 goroutine
先使用 Unlock
釋放鎖,則會觸發 panic
。不管被釋放的鎖是一個未被任何 goroutine
持有的鎖,還是正在被其它 goroutine
持有中的鎖。
所以,我們在使用互斥鎖 Mutex
時,遵循 “誰持有,誰釋放” 原則。
陷阱二
假如我們在使用 Mutex
時,只使用 Lock
持有鎖,而忘記使用 Unlock
釋放鎖,則會導致被阻塞中的 goroutine
一直被阻塞。
所以,我們在使用 Lock
時,可以在 mu.Lock()
後面,緊接著寫一行 defer mu.Unlock()
,當然,也要根據實際情況,靈活使用釋放鎖的方式,不一定必須使用 defer
的方式。
陷阱三
互斥鎖 Mutex
在被首次使用後,不可以複製。
func main() { var mu sync.Mutex var mu2 sync.Mutex go func(){ mu.Lock() defer mu.Unlock() fmt.Println("g1 goroutine") time.Sleep(time.Second * 10) }() time.Sleep(time.Second * 5) mu2 = mu mu2.Lock() fmt.Println("main goroutine") mu2.Unlock() }
閱讀程式碼,mu2 複製 mu 的值,程式會報錯,因為 mu
已經被 goroutine
呼叫,它的底層值已經發生變化,所以 mu2 得到的不是一個零值的 Mutex
。
不過該錯誤可以被 go vet
檢查到。
04
延伸
我們在文中使用的程式碼,可以很容易知道臨界區。但是,在實際專案中,我們會有一些複雜程式碼,即不太容易知道臨界區的程式碼。
此時,我們可以使用資料競爭檢測器,即 -race
,需要注意的是,它是在執行時進行資料競爭檢測,並且它比較耗費記憶體,在生產環境中不要使用。
使用方式:
go run -race main.go
05
總結
本文我們介紹 Go 併發程式設計中,經常會使用的 sync
標準庫中的互斥鎖 Mutex
。
文中的示例程式碼,未給出輸出結果,意在希望讀者朋友們可以親自動手執行程式碼,這樣可以幫助大家理解文章內容。