Go Wiki:程式碼檢閱:Go 並行處理
此頁面是 Go 程式碼檢閱意見 清單的補充。此清單的目標是協助在檢閱 Go 程式碼時找出與並行處理相關的錯誤。
您也可以只閱讀一次此清單,以喚醒記憶並確保您了解所有這些並行處理的陷阱。
⚠️ 此頁面由社群撰寫和維護。其中包含有爭議且可能具有誤導性或不正確的資訊。
同步不足與競爭條件
- HTTP 處理函式是否具備執行緒安全性?
- 全域函數和變數是否受互斥鎖保護或以其他方式執行緒安全?
- 讀取欄位和變數是否受保護?
- 迴圈變數是否作為引數傳遞到 goroutine 函數中?
- 執行緒安全類型的方法不會傳回受保護結構的指標嗎?
- 在
Load()
之後對sync.Map
進行的Load()
或Delete()
呼叫不是競爭條件嗎?
測試
可擴充性
時間
- 是否使用
defer tick.Stop()
停止time.Ticker
? - 使用
Equal()
而非==
來比較time.Time
嗎? - 在
time.Since()
的time.Time
引數中保留單調組成嗎? - 透過
t.Before(u)
比較系統時間時,是否從引數中移除單調組成?
同步不足與競爭條件
# RC.1. HTTP 處理常式函數是否可以從多個 goroutine 同時安全地呼叫?很容易忽略 HTTP 處理常式應為執行緒安全,因為它們通常不會在專案程式碼中的任何位置明確呼叫,而只會在 HTTP 伺服器的內部呼叫。
# RC.2. 是否有一些欄位或變數存取未受互斥鎖保護,其中欄位或變數是基本類型或非明確執行緒安全類型(例如 atomic.Value
),而此欄位可以從並發 goroutine 更新?即使是非原子硬體寫入和潛在記憶體可見度問題,略過同步讀取基本變數也不安全。
另請參閱 典型資料競爭:基本非受保護變數。
# RC.3. 執行緒安全類型的某個方法不會傳回受保護結構的指標嗎?這是導致前一項目中所述非受保護存取問題的微妙錯誤。範例
type Counters struct {
mu sync.Mutex
vals map[Key]*Counter
}
func (c *Counters) Add(k Key, amount int) {
c.mu.Lock()
defer c.mu.Unlock()
count, ok := c.vals[k]
if !ok {
count = &Counter{sum: 0, num: 0}
c.vals[k] = count
}
count.sum += amount
count.n += 1
}
func (c *Counters) GetCounter(k Key) *Counter {
c.mu.Lock()
defer c.mu.Unlock()
return c.vals[k] // BUG! Returns a pointer to the structure which must be protected
}
一種可能的解決方案是在 GetCounter()
中傳回副本,而不是結構的指標
type Counters struct {
mu sync.Mutex
vals map[Key]Counter // Note that now we are storing the Counters directly, not pointers.
}
...
func (c *Counters) GetCounter(k Key) Counter {
c.mu.Lock()
defer c.mu.Unlock()
return c.vals[k]
}
# RC.4. 如果有多個 goroutine 可以更新 sync.Map
,您不會根據前一個 m.Load()
呼叫的成功與否來呼叫 m.Store()
或 m.Delete()
嗎?換句話說,以下程式碼有競爭條件
var m sync.Map
// Can be called concurrently from multiple goroutines
func DoSomething(k Key, v Value) {
existing, ok := m.Load(k)
if !ok {
m.Store(k, v) // RACE CONDITION - two goroutines can execute this in parallel
... some other logic, assuming the value in `k` is now `v` in the map
}
...
}
在某些情況下,這種競爭條件可能是良性的:例如,Load()
和 Store()
呼叫之間的邏輯會計算要快取到映射中的值,而此計算總是會傳回相同的結果且沒有副作用。
⚠️ 可能具有誤導性的資訊。「競爭條件」可以指邏輯錯誤,例如這個例子,它可能是良性的。但這個詞組也常被用來指記憶體模型的違規,而這絕不會是良性的。
如果競爭條件不是良性的,請使用 sync.Map.LoadOrStore()
和 LoadAndDelete()
方法來修復它。
可擴充性
# Sc.1. 頻道是以零容量建立的,例如 make(chan *Foo)
,這是故意的嗎?將訊息傳送至零容量頻道的 goroutine 會被封鎖,直到另一個 goroutine 收到此訊息。在 make()
呼叫中省略容量可能只是一個錯誤,這會限制程式碼的可擴充性,而且單元測試很可能找不到這樣的錯誤。
⚠️ 具有誤導性的資訊。與非緩衝頻道相比,緩衝頻道並不會固有地增加「可擴充性」。然而,緩衝頻道很容易模糊死結和其他基本設計錯誤,而這些錯誤在非緩衝頻道中會立即顯現。
# Sc.2. 與單純的 sync.Mutex
相比,使用 RWMutex
進行鎖定會產生額外的開銷,此外,Go 中 RWMutex
的當前實作也可能存在一些 可擴充性問題。除非情況非常明確(例如用於同步許多持續數百毫秒或更久的唯讀操作的 RWMutex
,而需要獨佔鎖定的寫入操作很少發生),應有一些基準測試證明 RWMutex
確實有助於提升效能。一個 RWMutex
肯定弊大於利的典型範例是對結構中變數進行簡單的保護
type Box struct {
mu sync.RWMutex // DON'T DO THIS -- use a simple Mutex instead.
x int
}
func (b *Box) Get() int {
b.mu.RLock()
defer b.mu.RUnlock()
return b.x
}
func (b *Box) Set(x int) {
b.mu.Lock()
defer b.mu.Unlock()
b.x = x
}
時間
# Tm.1. 是否使用 defer tick.Stop()
停止 time.Ticker
?如果使用計時器迴圈的函式傳回時未停止計時器,就會造成記憶體外洩。
# Tm.2. 是否使用 Equal()
方法,而非 ==
,來比較 time.Time
結構?引用 time.Time
的文件
請注意,Go
==
算子不僅會比較時間點,還會比較位置和單調時鐘讀數。因此,不應將Time
值用作映射或資料庫金鑰,除非先保證已為所有值設定相同的「位置」,這可透過使用UTC()
或Local()
方法來達成,並透過設定t = t.Round(0)
來移除單調時鐘讀數。通常,優先使用t.Equal(u)
而非t == u
,因為t.Equal()
會使用最精確的可用比較,並正確處理其引數中只有一個具有單調時鐘讀數的情況。
# Tm.3. 在呼叫 time.Since(t)
之前,單調元件不會從 t
中移除?這是前一個項目的一個後果。如果在將 time.Time
結構傳遞到 time.Since()
函數之前(通過呼叫 UTC()
、Local()
、In()
、Round()
、Truncate()
或 AddDate()
)從該結構中移除了單調元件,則在極為罕見的情況下,例如在最初取得開始時間和呼叫 time.Since()
之間系統時間已通過 NTP 同步,time.Since()
的結果可能會為負。如果未移除單調元件,time.Since()
將始終傳回正持續時間。
# Tm.4. 如果您想通過 t.Before(u)
比較系統時間,您是否從參數中移除了單調元件,例如通過 u.Round(0)
?這是與 Tm.2 相關的另一個重點。有時,您需要僅按其中儲存的系統時間來比較兩個 time.Time
結構。在將其中一個 Time
結構儲存在磁碟上或通過網路傳送它們之前,您可能需要這樣做。例如,想像某種遙測代理程式會定期將遙測指標與時間推送到某個遠端系統
var latestSentTime time.Time
func pushMetricPeriodically(ctx context.Context) {
t := time.NewTicker(time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done: return
case <-t.C:
newTime := time.Now().Round(0) // Strip monotonic component to compare system time only
// Check that the new time is later to avoid messing up the telemetry if the system time
// is set backwards on an NTP sync.
if latestSentTime.Before(newTime) {
sendOverNetwork(NewDataPoint(newTime, metric()))
latestSentTime = newTime
}
}
}
}
如果不呼叫 Round(0)
,即移除單調元件,此程式碼將是錯誤的。
閱讀清單
Go 程式碼檢閱評論:一份檢閱 Go 程式碼的核對清單,不特定於並行處理。
Go 並行處理
- Go 記憶體模型
- Effective Go 中關於並行處理的部分
- Go 部落格中的文章
- 瞭解 Go 中的真實世界並行錯誤
並行,但並非特定於 Go
此內容是 Go Wiki 的一部分。