Go Wiki:程式碼檢閱:Go 並行處理

此頁面是 Go 程式碼檢閱意見 清單的補充。此清單的目標是協助在檢閱 Go 程式碼時找出與並行處理相關的錯誤。

您也可以只閱讀一次此清單,以喚醒記憶並確保您了解所有這些並行處理的陷阱。

⚠️ 此頁面由社群撰寫和維護。其中包含有爭議且可能具有誤導性或不正確的資訊。


同步不足與競爭條件

測試

可擴充性

時間


同步不足與競爭條件

# 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


此內容是 Go Wiki 的一部分。