中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何使用Go并發讀寫sync.map語句

發布時間:2021-10-15 10:23:30 來源:億速云 閱讀:149 作者:iii 欄目:開發技術

這篇文章主要介紹“如何使用Go并發讀寫sync.map語句”,在日常操作中,相信很多人在如何使用Go并發讀寫sync.map語句問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何使用Go并發讀寫sync.map語句”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

目錄
  • 1、sync.Map 優勢

  • 2、性能測試

    • 2.1 壓測結果

      • 1)寫入

      • 2)查找

      • 3)刪除

    • 2.3 場景分析

    • 3、sync.Map 剖析

      • 3.1 數據結構

        • 3.2 查找過程

          • 3.3 寫入過程

            • 3.4 刪除過程

            map 的兩種目前在業界使用的最多的并發支持的模式分別是:

            • 原生 map + 互斥鎖或讀寫鎖 mutex

            • 標準庫 sync.Map(Go1.9及以后)。

            有了選擇,總是有選擇困難癥的,這兩種到底怎么選,誰的性能更加的好?我有一個朋友說 標準庫 sync.Map 性能菜的很,不要用。我到底聽誰的...

            今天煎魚就帶你揭秘 Go sync.map,我們先會了解清楚什么場景下,Go map 的多種類型怎么用,誰的性能最好!

            接著根據各 map 性能分析的結果,針對性的對 sync.map 進行源碼解剖,了解 WHY。

            一起愉快地開始吸魚之路。

            1、sync.Map 優勢

            在 Go 官方文檔中明確指出 Map 類型的一些建議:

            如何使用Go并發讀寫sync.map語句

            • 多個 goroutine 的并發使用是安全的,不需要額外的鎖定或協調控制。

            • 大多數代碼應該使用原生的 map,而不是單獨的鎖定或協調控制,以獲得更好的類型安全性和維護性。

            同時 Map 類型,還針對以下場景進行了性能優化:

            • 當一個給定的鍵的條目只被寫入一次但被多次讀取時。例如在僅會增長的緩存中,就會有這種業務場景。

            • 當多個 goroutines 讀取、寫入和覆蓋不相干的鍵集合的條目時。

            這兩種情況與 Go map 搭配單獨的 Mutex RWMutex 相比較,使用 Map 類型可以大大減少鎖的爭奪。

            2、性能測試

            聽官方文檔介紹了一堆好處后,他并沒有講到缺點,所說的性能優化后的優勢又是否真實可信。我們一起來驗證一下。

            首先我們定義基本的數據結構:

            // 代表互斥鎖
            type FooMap struct {
             sync.Mutex
             data map[int]int
            }
            
            // 代表讀寫鎖
            type BarRwMap struct {
             sync.RWMutex
             data map[int]int
            }
            
            var fooMap *FooMap
            var barRwMap *BarRwMap
            var syncMap *sync.Map
            
            // 初始化基本數據結構
            func init() {
             fooMap = &FooMap{data: make(map[int]int, 100)}
             barRwMap = &BarRwMap{data: make(map[int]int, 100)}
             syncMap = &sync.Map{}
            }

            在配套方法上,常見的增刪改查動作我們都編寫了相應的方法。用于后續的壓測(只展示部分代碼):

            func builtinRwMapStore(k, v int) {
             barRwMap.Lock()
             defer barRwMap.Unlock()
             barRwMap.data[k] = v
            }
            
            func builtinRwMapLookup(k int) int {
             barRwMap.RLock()
             defer barRwMap.RUnlock()
             if v, ok := barRwMap.data[k]; !ok {
              return -1
             } else {
              return v
             }
            }
            
            func builtinRwMapDelete(k int) {
             barRwMap.Lock()
             defer barRwMap.Unlock()
             if _, ok := barRwMap.data[k]; !ok {
              return
             } else {
              delete(barRwMap.data, k)
             }
            }

            其余的類型方法基本類似,考慮重復篇幅問題因此就不在此展示了。

            壓測方法基本代碼如下:

            func BenchmarkBuiltinRwMapDeleteParalell(b *testing.B) {
             b.RunParallel(func(pb *testing.PB) {
              r := rand.New(rand.NewSource(time.Now().Unix()))
              for pb.Next() {
               k := r.Intn(100000000)
               builtinRwMapDelete(k)
              }
             })
            }

            這塊主要就是增刪改查的代碼和壓測方法的準備,壓測代碼直接復用的是大白大佬的 go19-examples/benchmark-for-map 項目。

            也可以使用 Go 官方提供的 map\_bench\_test.go,有興趣的小伙伴可以自己拉下來運行試一下。

            2.1 壓測結果

            1)寫入
            含義壓測結果
            BenchmarkBuiltinMapStoreParalell-4map+mutex 寫入元素237.1 ns/op
            BenchmarkSyncMapStoreParalell-4sync.map 寫入元素509.3 ns/op
            BenchmarkBuiltinRwMapStoreParalell-4map+rwmutex 寫入元素207.8 ns/op

            總體的排序(從慢到快)為:SyncMapStore < MapStore < RwMapStore。

            2)查找
            方法名含義壓測結果
            BenchmarkBuiltinMapLookupParalell-4map+mutex 查找元素166.7 ns/op
            BenchmarkBuiltinRwMapLookupParalell-4map+rwmutex 查找元素60.49 ns/op
            BenchmarkSyncMapLookupParalell-4sync.map 查找元素53.39 ns/op

            在查找元素上,最慢的是原生 map+互斥鎖,其次是原生 map+讀寫鎖。最快的是 sync.map 類型。

            總體的排序為:MapLookup < RwMapLookup < SyncMapLookup。

            3)刪除
            方法名含義壓測結果
            BenchmarkBuiltinMapDeleteParalell-4map+mutex 刪除元素168.3 ns/op
            BenchmarkBuiltinRwMapDeleteParalell-4map+rwmutex 刪除元素188.5 ns/op
            BenchmarkSyncMapDeleteParalell-4sync.map 刪除元素41.54 ns/op

            在刪除元素上,最慢的是原生 map+讀寫鎖,其次是原生 map+互斥鎖,最快的是 sync.map 類型。

            總體的排序為:RwMapDelete < MapDelete < SyncMapDelete

            2.3 場景分析

            根據上述的壓測結果,我們可以得出 sync.Map 類型:

            • 在讀和刪場景上的性能是最佳的,領先一倍有多。

            • 在寫入場景上的性能非常差,落后原生 map+鎖整整有一倍之多。

            因此在實際的業務場景中。假設是讀多寫少的場景,會更建議使用 sync.Map 類型。

            但若是那種寫多的場景,例如多 goroutine 批量的循環寫入,那就建議另辟途徑了,性能不忍直視(無性能要求另當別論)。

            3、sync.Map 剖析

            清楚如何測試,測試的結果后。我們需要進一步深挖,知其所以然。

            為什么 sync.Map 類型的測試結果這么的 “偏科”,為什么讀操作性能這么高,寫操作性能低的可怕,他是怎么設計的?

            3.1 數據結構

            sync.Map 類型的底層數據結構如下:

            type Map struct {
             mu Mutex
             read atomic.Value // readOnly
             dirty map[interface{}]*entry
             misses int
            }
            
            // Map.read 屬性實際存儲的是 readOnly。
            type readOnly struct {
             m       map[interface{}]*entry
             amended bool
            }
            • mu:互斥鎖,用于保護 read dirty

            • read:只讀數據,支持并發讀取(atomic.Value 類型)。如果涉及到更新操作,則只需要加鎖來保證數據安全。read 實際存儲的是 readOnly 結構體,內部也是一個原生 mapamended 屬性用于標記 read dirty 的數據是否一致。

            • dirty:讀寫數據,是一個原生 map,也就是非線程安全。操作 dirty 需要加鎖來保證數據安全。

            • misses:統計有多少次讀取 read 沒有命中。每次 read 中讀取失敗后,misses 的計數值都會加 1。

            read dirty 中,都有涉及到的結構體:

            type entry struct {
             p unsafe.Pointer // *interface{}
            }

            其包含一個指針 p, 用于指向用戶存儲的元素(key)所指向的 value 值。

            在此建議你必須搞懂 readdirtyentry,再往下看,食用效果會更佳,后續會圍繞著這幾個概念流轉。

            3.2 查找過程

            劃重點,Map 類型本質上是有兩個 “map”。一個叫 read、一個叫 dirty,長的也差不多:

            如何使用Go并發讀寫sync.map語句

            sync.Map 的 2 個 map

            當我們從 sync.Map 類型中讀取數據時,其會先查看 read 中是否包含所需的元素:

            • 若有,則通過 atomic 原子操作讀取數據并返回。

            • 若無,則會判斷 read.readOnly 中的 amended 屬性,他會告訴程序 dirty 是否包含 read.readOnly.m 中沒有的數據;因此若存在,也就是 amended 為 true,將會進一步到 dirty 中查找數據。

            sync.Map 的讀操作性能如此之高的原因,就在于存在 read 這一巧妙的設計,其作為一個緩存層,提供了快路徑(fast path)的查找。

            同時其結合 amended 屬性,配套解決了每次讀取都涉及鎖的問題,實現了讀這一個使用場景的高性能。

            3.3 寫入過程

            我們直接關注 sync.Map 類型的 Store 方法,該方法的作用是新增或更新一個元素。

            源碼如下:

            func (m *Map) Store(key, value interface{}) {
             read, _ := m.read.Load().(readOnly)
             if e, ok := read.m[key]; ok && e.tryStore(&value) {
              return
             }
              ...
            }

            調用 Load 方法檢查 m.read 中是否存在這個元素。若存在,且沒有被標記為刪除狀態,則嘗試存儲。

            若該元素不存在或已經被標記為刪除狀態,則繼續走到下面流程:

            func (m *Map) Store(key, value interface{}) {
             ...
             m.mu.Lock()
             read, _ = m.read.Load().(readOnly)
             if e, ok := read.m[key]; ok {
              if e.unexpungeLocked() {
               m.dirty[key] = e
              }
              e.storeLocked(&value)
             } else if e, ok := m.dirty[key]; ok {
              e.storeLocked(&value)
             } else {
              if !read.amended {
               m.dirtyLocked()
               m.read.Store(readOnly{m: read.m, amended: true})
              }
              m.dirty[key] = newEntry(value)
             }
             m.mu.Unlock()
            }

            由于已經走到了 dirty 的流程,因此開頭就直接調用了 Lock 方法上互斥鎖,保證數據安全,也是凸顯性能變差的第一幕。

            其分為以下三個處理分支:

            • 若發現 read 中存在該元素,但已經被標記為已刪除(expunged),則說明 dirty 不等于 nil(dirty 中肯定不存在該元素)。其將會執行如下操作。

            • 將元素狀態從已刪除(expunged)更改為 nil。

            • 將元素插入 dirty 中。

            • 若發現 read 中不存在該元素,但 dirty 中存在該元素,則直接寫入更新 entry 的指向。

            • 若發現 read dirty 都不存在該元素,則從 read 中復制未被標記刪除的數據,并向 dirty 中插入該元素,賦予元素值 entry 的指向。

            我們理一理,寫入過程的整體流程就是:

            • readread 上沒有,或者已標記刪除狀態。

            • 上互斥鎖(Mutex)。

            • 操作 dirty,根據各種數據情況和狀態進行處理。

            回到最初的話題,為什么他寫入性能差那么多。究其原因:

            • 寫入一定要會經過 read,無論如何都比別人多一層,后續還要查數據情況和狀態,性能開銷相較更大。

            • (第三個處理分支)當初始化或者 dirty 被提升后,會從 read 中復制全量的數據,若 read 中數據量大,則會影響性能。

            可得知 sync.Map 類型不適合寫多的場景,讀多寫少是比較好的。

            若有大數據量的場景,則需要考慮 read 復制數據時的偶然性能抖動是否能夠接受。

            3.4 刪除過程

            這時候可能有小伙伴在想了。寫入過程,理論上和刪除不會差太遠。怎么 sync.Map 類型的刪除的性能似乎還行,這里面有什么貓膩?

            源碼如下:

            func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
             read, _ := m.read.Load().(readOnly)
             e, ok := read.m[key]
             ...
              if ok {
              return e.delete()
             }
            }

            刪除是標準的開場,依然先到 read 檢查該元素是否存在。

            若存在,則調用 delete 標記為 expunged(刪除狀態),非常高效。可以明確在 read 中的元素,被刪除,性能是非常好的。

            若不存在,也就是走到 dirty 流程中:

            func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
             ...
             if !ok && read.amended {
              m.mu.Lock()
              read, _ = m.read.Load().(readOnly)
              e, ok = read.m[key]
              if !ok && read.amended {
               e, ok = m.dirty[key]
               delete(m.dirty, key)
               m.missLocked()
              }
              m.mu.Unlock()
             }
             ...
             return nil, false
            }

            read 中不存在該元素,dirty 不為空,read dirty 不一致(利用 amended 判別),則表明要操作 dirty,上互斥鎖。

            再重復進行雙重檢查,若 read 仍然不存在該元素。則調用 delete 方法從 dirty 中標記該元素的刪除。

            需要注意,出現頻率較高的 delete 方法:

            func (e *entry) delete() (value interface{}, ok bool) {
             for {
              p := atomic.LoadPointer(&e.p)
              if p == nil || p == expunged {
               return nil, false
              }
              if atomic.CompareAndSwapPointer(&e.p, p, nil) {
               return *(*interface{})(p), true
              }
             }
            }

            該方法都是將 entry.p 置為 nil,并且標記為 expunged(刪除狀態),而不是真真正正的刪除。

            注:不要誤用 sync.Map,前段時間從字節大佬分享的案例來看,他們將一個連接作為 key 放了進去,于是和這個連接相關的,例如:buffer 的內存就永遠無法釋放了...

            到此,關于“如何使用Go并發讀寫sync.map語句”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

            向AI問一下細節

            免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

            go
            AI

            安新县| 名山县| 丹棱县| 安丘市| 平安县| 比如县| 平邑县| 灌云县| 丰原市| 刚察县| 舞阳县| 临西县| 榆中县| 元阳县| 靖宇县| 崇阳县| 阿巴嘎旗| 宣化县| 霞浦县| 革吉县| 峨山| 新巴尔虎左旗| 翁源县| 福海县| 柘荣县| 慈溪市| 广昌县| 宜都市| 吉安市| 宜阳县| 天长市| 宜城市| 丰县| 黎城县| 银川市| 永顺县| 翁源县| 常熟市| 辽宁省| 宁明县| 屏边|