Go 語言併發編程之互斥鎖 sync-Mutex
大家好,我是 frank。「Golang 語言開發棧」公衆號作者。
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
。
文中的示例代碼,未給出輸出結果,意在希望讀者朋友們可以親自動手執行代碼,這樣可以幫助大家理解文章內容。
參考資料
[1] sync.Mutex
源碼: https://github.com/golang/go/blob/master/src/sync/mutex.go
[2] the Go memory model: https://go.dev/ref/mem
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/aYygHWntBAR08DDr1PpFTw