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 提供了三個方法,分別是 LockUnlock 和 Go 1.18 新增的 TryLock

我們可以使用 MutexLock 方法獲取鎖,獲取鎖的 goroutine 可以訪問和修改臨界區,此時其它 goroutine 如果也想要訪問和修改臨界區,則會被阻塞,等待當前獲取鎖的 goroutine 釋放鎖。

持有鎖的 goroutine 釋放鎖,可以使用  MutexUnlock 方法。

細心的讀者朋友們,可能已經發現,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