Go: 併發訪問 Map — Part III

在上一篇文章 “Go: 通過源碼研究 Map 的設計 [1]” 中,我們講述了 map 的內部實現。

Go blog[2] 中專門講解 map 的文章明確地表明:

map 是非併發安全的 [3]:併發讀寫 map 時,map 的行爲是未知的。如果你需要使用併發執行的 Goroutine 同時讀寫 map,必須使用某種同步機制來協調訪問。

然而,正如 FAQ[4] 中解釋的,Google 提供了一些幫助:

作爲一種糾正 map 使用方式的輔助手段,語言的某些實現包含了特殊的檢查,當運行時的 map 被不安全地併發修改時,它會自動報告。

數據爭用檢測

我們可以從 Go 獲得的第一個幫助就是數據爭用檢測。使用 -race 標記來運行你的程序或測試會讓你瞭解潛在的數據爭用。讓我們看一個例子:

func main() {
   m := make(map[string]int, 1)
   m[`foo`] = 1

   var wg sync.WaitGroup

   wg.Add(2)
   Go func() {
      for i := 0; i < 1000; i++  {
         m[`foo`]++
      }
   }()
   Go func() {
      for i := 0; i < 1000; i++  {
         m[`foo`]++
      }
   }()
   wg.Wait()
}

在這個例子中,我們清晰地看到,在某一時刻,兩個 Goroutine 嘗試同時寫入一個新值。下面是爭用檢測器的輸出:

==================
WARNING: DATA RACE
Read at 0x00c00008e000 by Goroutine 6:
   runtime.mapaccess1_faststr()
      /usr/local/go/src/runtime/map_faststr.go:12 +0x0
   main.main.func2()
      main.go:19 +0x69

Previous write at 0x00c00008e000 by Goroutine 5:
   runtime.mapassign_faststr()
      /usr/local/go/src/runtime/map_faststr.go:202 +0x0
   main.main.func1()
      main.go:14 +0xb8

爭用檢測器解釋道,當第二個 Goroutine 正在讀變量時,第一個 Goroutine 正在向同一個內存地址寫一個新值。如果你想要了解更多,我建議你閱讀我的一篇關於數據爭用檢測器 [5] 的文章。

併發寫入檢測

Go 提供的另一個幫助是併發寫入檢測。讓我們使用之前看到的那個例子。運行這個程序時,我們將看到一個錯誤:

fatal error: concurrent map writes

在 map 結構的內部標誌 flags 的幫助下,Go 處理了這次併發。當代碼嘗試修改 map 時(賦值,刪除值或者清空 map),flags 的某一位會被置爲 1:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
   [...]
   h.flags ^= hashWriting

值爲 4 的 hashWriting 會將相關的位置爲 1。

^ 是一個異或操作,如果兩個操作數的某一位的值不同,^ 將該位置爲 1:

img

當操作結束時,該標誌會被重置:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
   [...]
   h.flags &^= hashWriting
}

既然每個修改 map 的操作都設置了一個控制標誌,那麼通過檢查這個標誌的狀態,就可以防止併發寫入。這裏是該標誌的生命週期的例子:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
   [...]
   // if another process is currently writing, throw error
   if h.flags&hashWriting != 0 {
      throw("concurrent map writes")
   }
   [...]
   // no one is writing, we can set now the flag
   h.flags ^= hashWriting
   [...]
   // flag reset
   h.flags &^= hashWriting
}

sync.Map vs Map with lock

sync 包也提供了併發安全的 map。不過,正如文檔 [6] 中解釋的,你應該謹慎的選擇你使用的 map:

sync 包中的 map 類型是專業的。大多數代碼應該使用原生的 Go map,附加上鎖或者其他協調方式,這樣類型安全更有保障,而且更容易維護其他的不變量和 map 的內容。

實際上,正如我的文章 “Go: 通過源碼研究 Map 的設計 [7]” 中所解釋的,map 根據我們處理的具體類型提供了不同的方法。

讓我們運行一個簡單的基準測試,比較帶有鎖的常規 map 和 sync 包的 map。一個基準測試併發寫入 map,另一個僅僅讀取 map 中的值:

MapWithLockWithWriteOnlyInConcurrentEnc-8  68.2 µ s ± 2%
SyncMapWithWriteOnlyInConcurrentEnc-8       192 µ s ± 2%
MapWithLockWithReadOnlyInConcurrentEnc-8   76.8 µ s ± 3%
SyncMapWithReadOnlyInConcurrentEnc-8       55.7 µ s ± 4%

我們可以看到,兩種 map 各有千秋。我們可以根據具體的情況選擇其中之一。文檔 [8] 中很好地解釋了這些情況:

map 類型針對兩種常見使用場景做了優化:(1) 指定 key 的 entry 僅寫入一次,但多次讀取,比如只增長的緩存;(2) 多個 Goroutine 讀取、寫入、覆蓋不相交的 key 的集合指向的 entry。

Map vs sync.Map

FAQ[9] 中也解釋了他們做出了默認情況下 map 非併發安全這個決定的原因:

因此,要求所有的 map 操作都獲取互斥鎖,會拖慢大多數程序,但只爲很少的程序增加了安全性

讓我們運行一個不使用併發 Goroutine 的基準測試,來理解當你不需要併發但標準庫默認提供併發安全的 map 時,可能帶來的影響:

MapWithWriteOnly-8          11.1ns ± 3%
SyncMapWithWriteOnly-8       121ns ± 6%

MapWithReadOnly-8           4.87ns ± 7%
SyncMapWithReadOnly-8       29.2ns ± 4%

簡單的 map 快 7 到 10 倍。顯然,在非併發模式下,這聽起來更合理,巨大的差異也清楚的解釋了爲什麼默認非併發安全的 map 是更好的選擇。如果你不需要處理併發狀況,爲什麼要讓程序運行的更慢呢?


via: https://medium.com/@blanchon.vincent/go-concurrency-access-with-maps-part-iii-8c0a0e4eb27e

作者:blanchon.vincent[10] 譯者:DoubleLuck[11] 校對:dingdingzhou[12]

本文由 GCTT[13] 原創編譯,Go 中文網 [14] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

Go: 通過源碼研究 Map 的設計: https://studygolang.com/articles/22777

[2]

Go blog: https://blog.golang.org/go-maps-in-action

[3]

map 是非併發安全的: https://golang.org/doc/faq#atomic_maps

[4]

FAQ: https://golang.org/doc/faq#atomic_maps

[5]

數據爭用檢測器: https://medium.com/@blanchon.vincent/go-race-detector-with-threadsanitizer-8e497f9e42db

[6]

文檔: https://golang.org/pkg/sync/

[7]

Go: 通過源碼研究 Map 的設計: https://studygolang.com/articles/22777

[8]

文檔: https://golang.org/pkg/sync/#Map

[9]

FAQ: https://golang.org/doc/faq#atomic_maps

[10]

blanchon.vincent: https://medium.com/@blanchon.vincent

[11]

DoubleLuck: https://github.com/DoubleLuck

[12]

dingdingzhou: https://github.com/dingdingzhou

[13]

GCTT: https://github.com/studygolang/GCTT

[14]

Go 中文網: https://studygolang.com/

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