Go 語言併發程式設計之互斥鎖詳解 sync.Mutex

林台山人發表於2024-09-29

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

文中的示例程式碼,未給出輸出結果,意在希望讀者朋友們可以親自動手執行程式碼,這樣可以幫助大家理解文章內容。

相關文章