Go: Copying a sync type

複製 sync 包中類型的變量時需小心謹慎

sync 包提供了基本的同步原語,像互斥鎖 (sync.Mutex)、條件變量(sync.Cond)、等待組(sync.WaitGroup) 等。對於所有這些類型,有一條硬性規則需要我們遵守:不能對這些類型的變量進行復制使用。本文討論它們的工作原理以及如果進行復制使用會導致什麼問題。

下面程序實現了一個計數存儲功能,並且是線程安全的。Counter結構中的map[string]int表示每個計數器的當前值,爲了保證其併發訪問操作的安全性,使用 sync.Mutex 保護它,Add 方法實現計數增加功能。代碼如下, 對 counters 計數放在臨界區中,即放在c.mu.Lock()c.mu.Unlock()中。

type Counter struct {
        mu       sync.Mutex
        counters map[string]int
}

func NewCounter() Counter {
        return Counter{counters: map[string]int{}}
}

func (c Counter) Increment(name string) {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.counters[name]++
}

下面啓動兩個 goroutine 對 counter 進行自增操作,在運行時加入 - race 參數進行數據競爭檢查,看看會產生什麼情況。

counter := NewCounter()

go func() {
        counter.Increment("foo")
}()
go func() {
        counter.Increment("bar")
}()

完整代碼見 https://github.com/ThomasMing0915/100-go-mistakes-code/tree/main/74,程序輸出結果如下。什麼?,竟然存在數據競爭?

go run -race example1.go                                               
==================
WARNING: DATA RACE
Read at 0x00c0000a0060 by goroutine 7:
  runtime.mapaccess1_faststr()

上述代碼存在數據競爭的原因是互斥鎖 (mu) 被複制了,因爲 Increment 操作的接收者是值類型,所以當我們每次調用 Increment 時,它都會對當前的 Counter 進行復制,Counter 內部的互斥鎖也被複制了。sync 包中的類型不能被複制使用,像下面列舉的類型都是不能進行復制使用的.

既然知道了上述程序問題所在,現在的問題是如何解決呢?主要有兩種解決方法。

第一種解決方法是,將 Increment 方法的接收者從值類型改爲指針類型,代碼如下。通過修改接收者類型,可以避免調用 Increment 時複製 Counter,進而避免內部互斥鎖複製。

func (c *Counter) Increment(name string) {
        // Same code
}

第二種解法方法是,如果不想調整接收者的類型,可以將 Counter 結構體中 mu 字段的類型改爲指針類型,代碼如下。雖然 Increment 的接收者類型還是值類型,調用時會複製 Counter 結構,但是由於 mu 是一個指針,複製後指針指向的對象和被複制對象指針指向都是同一個對象,所以不存在數據競爭問題。

type Counter struct {
        mu       *sync.Mutex
        counters map[string]int
}

func NewCounter() Counter {
        return Counter{
                mu: &sync.Mutex{},
                counters: map[string]int{},
        }
}

「NOTE: 在第二種解決方法中,我們將 mu 字段定義爲指針類型,這個時候在創建 Counter 時需要進行初始化。如果省略它,mu 的值會被初始化爲指針的零值 (nil),在調用 c.mu.Lock() 會產生 panic.」

在下面的情況中,我們可能會遇到無意複製 sync 字段的問題,在編寫程序時應該小心謹慎。

此外,使用一些靜態代碼檢查工具 linter 可以掃描出這類問題。例如,使用 go vet 檢查前面的程序,輸出結果如下:

go vet .
./example1.go:19:9: Increment passes lock by value: Counter contains sync.Mutex

總結:當多個 goroutine 需要訪問一個公共的 sync 包中對象時,我們必須確保它們都依賴於同一個實例。該規則適用於 sync 包定義的所有類型,使用指針而不是值是解決這種問題的一個方法:將結構體中用到的 sync 包中類型的字段定義爲指針類型,或者使用結構體的指針對象。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pkgD1mMof0spFeYKklD2wQ