圖解Go裏面的sync.Map瞭解編程語言核心實現源碼

基礎築基

在大多數語言中原始map都不是一個線程安全的數據結構,那若是要在多個線程或者goroutine中對線程進行更改就須要加鎖,除了加1個大鎖,不一樣的語言還有不一樣的優化方式, 像在java和go這種語言其實都採用的是鏈表法來進行map的實現,本文也主要分析這種場景java

併發安全的map實現的三種方式

在go語言中實現多個goroutine併發安全訪問修改的map的方式,主要有以下三種:安全

實現方式 原理 適用場景
map+Mutex 經過Mutex互斥鎖來實現多個goroutine對map的串行化訪問 讀寫都須要經過Mutex加鎖和釋放鎖,適用於讀寫比接近的場景
map+RWMutex 經過RWMutex來實現對map的讀寫進行讀寫鎖分離加鎖,從而實現讀的併發性能提升 同Mutex相比適用於讀多寫少的場景
sync.Map 底層通分離讀寫map和原子指令來實現讀的近似無鎖,並經過延遲更新的方式來保證讀的無鎖化 讀多修改少,元素增長刪除頻率不高的狀況,在大多數狀況下替代上述兩種實現

上面三種實現具體的性能差別可能還要針對不一樣的具體的業務場景和平臺、數據量等所以來進行綜合的測試,源碼的學習更多的是瞭解其實現細節,以便在出現性能瓶頸的時候能夠進行分析,找出解決解決方案微信

map的容量問題

image.png
在Mutex和RWMutex實現的併發安全的map中map隨着時間和元素數量的增長、刪除,容量會不斷的遞增,在某些狀況下好比在某個時間點頻繁的進行大量數據的增長,而後又大量的刪除,其map的容量並不會隨着元素的刪除而縮小,而在sync.Map中,當進行元素從dirty進行提高到read map的時候會進行重建,可能會縮容數據結構

無鎖讀與讀寫分離

image.png

讀寫分離

併發訪問map讀的主要問題實際上是在擴容的時候,可能會致使元素被hash到其餘的地址,那若是個人讀的map不會進行擴容操做,就能夠進行併發安全的訪問了,而sync.map裏面正是採用了這種方式,對增長元素經過dirty來進行保存併發

無鎖讀

經過read只讀和dirty寫map將操做分離,其實就只須要經過原子指令對read map來進行讀操做而不須要加鎖了,從而提升讀的性能ide

寫加鎖與延遲提高

image.png

寫加鎖

上面提到增長元素操做可能會先增長大dirty寫map中,那針對多個goroutine同時寫,其實就須要進行Mutex加鎖了源碼分析

延遲提高

上面提到了read只讀map和dirty寫map, 那就會有個問題,默認增長元素都放在dirty中,那後續訪問新的元素若是都經過 mutex加鎖,那read只讀map就失去意義,sync.Map中採用一直延遲提高的策略,進行批量將當前map中的全部元素都提高到read只讀map中從而爲後續的讀訪問提供無鎖支持性能

指針與惰性刪除

image.png

map裏面的指針

map裏面存儲數據都會涉及到一個問題就是存儲值仍是指針,存儲值可讓 map做爲一個大的的對象,減輕垃圾回收的壓力(避免掃描全部小對象),而存儲指針能夠減小內存利用,而sync.Map中其實採用了指針結合惰性刪除的方式,來進行 map的value的存儲學習

惰性刪除

惰性刪除是併發設計中一中常見的設計,好比刪除某個個鏈表元素,若是要刪除則須要修改先後元素的指針,而採用惰性刪除,則一般只須要給某個標誌位設定爲刪除,而後在後續修改中再進行操做,sync.Map中也採用這種方式,經過給指針指向某個標識刪除的指針,從而實現惰性刪除測試

源碼分析

數據結構分析

Map

type Map struct {
    mu Mutex
    // read是一個readOnly的指針,裏面包含了一個map結構,就是咱們說的只讀map對該map的元素的訪問
    // 不須要加鎖,只須要經過atomic加載最新的指針便可
    read atomic.Value // readOnly

    // dirty包含部分map的鍵值對,若是要訪問須要進行mutex獲取
    // 最終dirty中的元素會被所有提高到read裏面的map中
    dirty map[interface{}]*entry

   // misses是一個計數器用於記錄從read中沒有加載到數據
    // 嘗試從dirty中進行獲取的次數,從而決定將數據從dirty遷移到read的時機
    misses int
}

readOnly

只讀map,對該map元素的訪問不須要加鎖,可是該map也不會進行元素的增長,元素會被先添加到dirty中而後後續再轉移到read只讀map中,經過atomic原子操做不須要進行鎖操做

type readOnly struct {
    // m包含全部只讀數據,不會進行任何的數據增長和刪除操做
    // 可是能夠修改entry的指針由於這個不會致使map的元素移動
    m       map[interface{}]*entry
    // 標誌位,若是爲true則代表當前read只讀map的數據不完整,dirty map中包含部分數據
    amended bool 
}

entry

entry是sync.Map中值得指針,若是當p指針指向expunged這個指針的時候,則代表該元素被刪除,但不會當即從map中刪除,若是在未刪除以前又從新賦值則會重用該元素

type entry struct {
    // 指向元素實際值得指針
    p unsafe.Pointer // *interface{}
}

數據的存儲

image.png

2.2.1 元素存在只讀map

元素若是存儲在只讀map中,則只須要獲取entry元素,而後修改其p的指針指向新的元素就能夠了,由於是原地操做因此map不會發生變化

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

元素存在進行變動後的只讀map中

若是此時發現元素存在只讀 map中,則證實以前有操做觸發了從dirty到read map的遷移,若是此時發現存在則修改指針便可

read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() {
            // The entry was previously expunged, which implies that there is a
            // non-nil dirty map and this entry is not in it.
            // 若是key以前已經被刪除,則這個地方會將key從進行nil覆蓋以前已經刪除的指針
            // 而後將它加入到dirty中
            m.dirty[key] = e
        }
        // 調用atomic進行value存儲
        e.storeLocked(&value)
    }

元素存在dirty map中

若是元素存在dirty中其實同read map邏輯同樣,只須要修改對應元素的指針便可

} else if e, ok := m.dirty[key]; ok {
        // 若是已經在dirty中就會直接存儲
        e.storeLocked(&value)
    } else {

元素以前不存在

若是元素以前不存在當前Map中則須要先將其存儲在dirty map中,同時將amended標識爲true,即當前read中的數據不全,有一部分數據存儲在dirty中

// 若是當前不是在修正狀態
        if !read.amended {               
            // 新加入的key會先被添加到dirty map中, 並進行read標記爲不完整
            // 若是dirty爲空則將read中的全部沒有被刪除的數據都遷移到dirty中
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)

數據遷移

read map到dirty map的遷移

image.png
在剛初始化和將全部元素遷移到read中後,dirty默認都是nil元素,而此時若是有新的元素增長,則須要先將read map中的全部未刪除數據先遷移到dirty中

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
        }
    }
}

dirty到read map的遷移

image.png
當持續的從read訪問穿透到dirty中後,就會觸發一次從dirty到read的遷移,這也意味着若是咱們的元素讀寫比差比較小,其實就會致使頻繁的遷移操做,性能其實可能並不如rwmutex等實現

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
}

數據加載

image.png

只讀無鎖

Load數據的時候回先從read中獲取,若是此時發現元素,則直接返回便可

read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]

加鎖讀取read和dirty

加鎖後會嘗試從read和dirty中讀取,同時進行misses計數器的遞增,若是知足遷移條件則會進行數據遷移

read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 這裏將採起緩慢遷移的策略
            // 只有當misses計數==len(m.dirty)的時候,纔會將dirty裏面的數據所有晉升到read中
            m.missLocked()
        }

數據刪除

image.png
數據刪除則分爲兩個過程,若是數據在read中,則就直接修改entry的標誌位指向刪除的指針便可,若是當前read中數據不全,則須要進行dirty裏面的元素刪除嘗試,若是存在就直接從dirty中刪除便可

func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    if ok {
        e.delete()
    }
}

mutex與加鎖後的read map重複讀

由於mutex互斥的是全部操做,包括dirty map的修改、數據的遷移、刪除,若是在進行m.lock的時候,已經有一個提高dirty到read操做在進行,則執行完成後dirty其實是沒有數據的,因此此時要再次進行read的重複讀

微信號:baxiaoshi2020

關注公告號閱讀更多源碼分析文章21天大棚

更多文章關注 www.sreguide.com
本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索