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 包中的類型不能被複制使用,像下面列舉的類型都是不能進行復制使用的.
-
sync.Cond
-
sync.Map
-
sync.Mutex
-
sync.RWMutex
-
sync.Once
-
sync.Pool
-
sync.WaitGroup
既然知道了上述程序問題所在,現在的問題是如何解決呢?主要有兩種解決方法。
第一種解決方法是,將 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 字段的問題,在編寫程序時應該小心謹慎。
-
調用值接收器的方法(像本文中的例子),值對象結構體定義中含有 sync 包中類型
-
將 sync 包中的類型變量作爲函數入參傳遞
-
函數入參變量類型結構體定義中含有 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