看過這篇剖析,你還不懂 Go sync.Map 嗎横侦?

hi, 大家好挥萌,我是 haohongfan。

本篇文章會從使用方式和源碼角度剖析 sync.Map枉侧。不過不管是日常開發(fā)還是開源項目中引瀑,好像 sync.Map 并沒有得到很好的利用,大家還是習慣使用 Mutex + Map 來使用榨馁。

下面這段代碼憨栽,看起來很有道理,其實是用錯了(背景:并發(fā)場景中獲取注冊信息)翼虫。

instance, ok := instanceMap[name]
if ok {
    return instance, nil
}

initLock.Lock()
defer initLock.Unlock()

// double check
instance, ok = instanceMap[name]
if ok {
    return instance, nil
}

這里使用使用 sync.Map 會更合理些屑柔,因為 sync.Map 底層完全包含了這個邏輯≌浣#可能寫 Java 的同學看著上面這段代碼很眼熟掸宛,但確實是用錯了,關于為什么用錯了以及會造成什么影響招拙,請大家關注后續(xù)的文章唧瘾。

我大概分析了下大家寧愿使用 Mutex + Map措译,也不愿使用 sync.Map 的原因:

  1. sync.Map 本身就很難用,使用起來并不像一個 Map饰序。失去了 map 應有的特權語法领虹,如:make, map[1] 等
  2. sync.Map 方法較多。讓一個簡單的 Map 使用起來有了較高的學習成本求豫。

不管什么樣的原因吧塌衰,當你讀過這篇文章后,在某些特定的并發(fā)場景下蝠嘉,建議使用 sync.Map 代替 Map + Mutex 的猾蒂。

用法全解

package main

import (
    "fmt"
    "sync"
)

func main() {
    var syncMap sync.Map
    syncMap.Store("11", 11)
    syncMap.Store("22", 22)

    fmt.Println(syncMap.Load("11")) // 11
    fmt.Println(syncMap.Load("33")) // 空

    fmt.Println(syncMap.LoadOrStore("33", 33)) // 33
    fmt.Println(syncMap.Load("33")) // 33
    fmt.Println(syncMap.LoadAndDelete("33")) // 33
    fmt.Println(syncMap.Load("33")) // 空

    syncMap.Range(func(key, value interface{}) bool {
        fmt.Printf("key:%v value:%v\n", key, value)
        return true
    })
    // key:22 value:22
    // key:11 value:11
}

其實 sync.Map 并不復雜,只是將普通 map 的相關操作轉成對應函數(shù)而已是晨。

普通 map sync.Map
map 獲取某個 key map[1] sync.Load(1)
map 添加元素 map[1] = 10 sync.Store(1, 10)
map 刪除一個 key delete(map, 1) sync.Delete(1)
遍歷 map for...range sync.Range()

sync.Map 兩個特有的函數(shù)肚菠,不過從字面就能理解是什么意思了。
LoadOrStore:sync.Map 存在就返回罩缴,不存在就插入
LoadAndDelete:sync.Map 獲取某個 key蚊逢,如果存在的話,同時刪除這個 key

源碼解析

type Map struct {
    mu Mutex
    read atomic.Value // readOnly  read map
    dirty map[interface{}]*entry  // dirty map
    misses int
}
sync map 全景圖
Load
Store
Delete

read map 的值是什么時間更新的 箫章?

  1. Load/LoadOrStore/LoadAndDelete 時烙荷,當 misses 數(shù)量大于等于 dirty map 的元素個數(shù)時,會整體復制 dirty map 到 read map
  2. Store/LoadOrStore 時檬寂,當 read map 中存在這個key终抽,則更新
  3. Delete/LoadAndDelete 時,如果 read map 中存在這個key桶至,則設置這個值為 nil

dirty map 的值是什么時間更新的 昼伴?

  1. 完全是一個新 key, 第一次插入 sync.Map镣屹,必先插入 dirty map
  2. Store/LoadOrStore 時圃郊,當 read map 中不存在這個key,在 dirty map 存在這個key女蜈,則更新
  3. Delete/LoadAndDelete 時持舆,如果 read map 中不存在這個key,在 dirty map 存在這個key伪窖,則從 dirty map 中刪除這個key
  4. 當 misses 數(shù)量大于等于 dirty map 的元素個數(shù)時逸寓,會整體復制 dirty map 到 read map,同時設置 dirty map 為 nil

疑問:當 dirty map 復制到 read map 后覆山,將 dirty map 設置為 nil竹伸,也就是 dirty map 中就不存在這個 key 了。如果又新插入某個 key汹买,多次訪問后達到了 dirty map 往 read map 復制的條件佩伤,如果直接用 read map 覆蓋 dirty map聊倔,那豈不是就丟了之前在 read map 但不在 dirty map 的 key ?

答:其實并不會晦毙。當 dirty map 向 read map 復制后生巡,readOnly.amended 等于了 false。當新插入了一個值時见妒,會將 read map 中的值孤荣,重新給 dirty map 賦值一遍,也就是 read map 也會向 dirty map 中復制须揣。

func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }

    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[interface{}]*entry, len(read.m))
    for k, e := range read.m {
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

read map 和 dirty map 是什么時間刪除的盐股?

  • 當 read map 中存在某個 key 的時候,這個時候只會刪除 read map耻卡, 并不會刪除 dirty map(因為 dirty map 不存在這個值)
  • 當 read map 中不存在時疯汁,才會去刪除 dirty map 里面的值

疑問:如果按照這個刪除方式,那豈不是 dirty map 中會有殘余的 key卵酪,導致沒刪除掉幌蚊?

答:其實并不會。當 misses 數(shù)量大于等于 dirty map 的元素個數(shù)時溃卡,會整體復制 dirty map 到 read map溢豆。這個過程中還附帶了另外一個操作:將 dirty map 置為 nil。

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

read map 與 dirty map 的關系 瘸羡?

  1. 在 read map 中存在的值漩仙,在 dirty map 中可能不存在。
  2. 在 dirty map 中存在的值犹赖,在 read map 中也可能存在队他。
  3. 當訪問多次,發(fā)現(xiàn) dirty map 中存在峻村,read map 中不存在漱挎,導致 misses 數(shù)量大于等于 dirty map 的元素個數(shù)時,會整體復制 dirty map 到 read map雀哨。
  4. 當出現(xiàn) dirty map 向 read map 復制后磕谅,dirty map 會被置成 nil。
  5. 當出現(xiàn) dirty map 向 read map 復制后雾棺,readOnly.amended 等于了 false膊夹。當新插入了一個值時,會將 read map 中的值捌浩,重新給 dirty map 賦值一遍

read/dirty map 中的值一定是有效的嗎放刨?

并不一定。放入到 read/dirty map 中的值總共有 3 種類型:

  • nil:如果獲取到的 value 是 nil尸饺,那說明這個 key 是已經(jīng)刪除過的进统。既不在 read map助币,也不在 dirty map
  • expunged:這個 key 在 dirty map 中是不存在的
  • valid:其實就正常的情況,要么這個值存在在 read map 中螟碎,要么存在在 dirty map 中

sync.Map 是如何提高性能的眉菱?

通過源碼解析,我們知道 sync.Map 里面有兩個普通 map掉分,read map主要是負責讀俭缓,dirty map 是負責讀和寫(加鎖)。在讀多寫少的場景下酥郭,read map 的值基本不發(fā)生變化华坦,可以讓 read map 做到無鎖操作,就減少了使用 Mutex + Map 必須的加鎖/解鎖環(huán)節(jié)不从,因此也就提高了性能惜姐。

不過也能夠看出來,read map 也是會發(fā)生變化的椿息,如果某些 key 寫操作特別頻繁的話歹袁,sync.Map 基本也就退化成了 Mutex + Map(有可能性能還不如 Mutex + Map)。

所以撵颊,不是說使用了 sync.Map 就一定能提高程序性能宇攻,我們日常使用中盡量注意拆分粒度來使用 sync.Map。

關于如何分析 sync.Map 是否優(yōu)化了程序性能倡勇,同樣可以使用 pprof逞刷。具體過程可以參考 《這可能是最容易理解的 Go Mutex 源碼剖析》

sync.Map 應用場景

  1. 讀多寫少
  2. 寫操作也多,但是修改的 key 和讀取的 key 特別不重合妻熊。

關于第二點我覺得挺扯的夸浅,畢竟我們很難把控這一點,不過由于是官方的注釋還是放在這里扔役。

實際開發(fā)中我們要注意使用場景和擅用 pprof 來分析程序性能帆喇。

sync.Map 使用注意點

和 Mutex 一樣, sync.Map 也同樣不能被復制亿胸,因為 atomic.Value 是不能被復制的坯钦。

參考鏈接

  1. https://golang.design/under-the-hood/zh-cn/part1basic/ch05sync/map/
  2. https://draveness.me/golang-sync-primitives/
  3. https://github.com/golang/go/blob/master/src/sync/map.go

sync.Map 完整流程圖獲取鏈接:鏈接: https://pan.baidu.com/s/1RIX6NKj8UhWkdFyFHptWwg 密碼: rsg9。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末侈玄,一起剝皮案震驚了整個濱河市婉刀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌序仙,老刑警劉巖突颊,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡律秃,警方通過查閱死者的電腦和手機爬橡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來棒动,“玉大人糙申,你說我怎么就攤上這事∏停” “怎么了郭宝?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵辞槐,是天一觀的道長掷漱。 經(jīng)常有香客問我,道長榄檬,這世上最難降的妖魔是什么卜范? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鹿榜,結果婚禮上海雪,老公的妹妹穿的比我還像新娘。我一直安慰自己舱殿,他們只是感情好奥裸,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沪袭,像睡著了一般湾宙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上冈绊,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天侠鳄,我揣著相機與錄音,去河邊找鬼死宣。 笑死伟恶,一個胖子當著我的面吹牛,可吹牛的內容都是我干的毅该。 我是一名探鬼主播博秫,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼眶掌!你這毒婦竟也來了挡育?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤畏线,失蹤者是張志新(化名)和其女友劉穎静盅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡蒿叠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年明垢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片市咽。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡痊银,死狀恐怖,靈堂內的尸體忽然破棺而出施绎,到底是詐尸還是另有隱情溯革,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布谷醉,位于F島的核電站致稀,受9級特大地震影響,放射性物質發(fā)生泄漏俱尼。R本人自食惡果不足惜抖单,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望遇八。 院中可真熱鬧矛绘,春花似錦、人聲如沸刃永。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斯够。三九已至囚玫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間雳刺,已是汗流浹背劫灶。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掖桦,地道東北人本昏。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像枪汪,于是被迫代替她去往敵國和親涌穆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容

  • 偶然看見這么篇文章:一道并發(fā)和鎖的golang面試題雀久。 雖然年代久遠宿稀,但也稍有興趣。 正好最近也看到了 sync....
    一塊牛排閱讀 1,852評論 0 1
  • map并發(fā)讀線程安全赖捌,并發(fā)讀寫線程不安全祝沸。 sync.Map 讀寫分離 空間換時間 Map Golang1.6之前...
    JunChow520閱讀 250評論 0 0
  • map map的底層實現(xiàn) golang中的map采用了HashTable的實現(xiàn),通過數(shù)組+鏈表實現(xiàn)的。一個哈希表會...
    xixisuli閱讀 3,687評論 0 1
  • [TOC] 本文基于1.10源碼分析如之前的文章可以看到罩锐,golang中的map是不支持并發(fā)操作的奉狈,golang推...
    123archu閱讀 8,468評論 1 8
  • 版本 go version 1.10.1 使用方法 數(shù)據(jù)結構 mu: Mutex鎖,在對dirty進行操作的時候涩惑,...
    不就是個名字么不要在意閱讀 537評論 0 0