溫馨提示×

溫馨提示×

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

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

如何理解Go里面的sync.Map

發(fā)布時間:2021-10-12 11:13:59 來源:億速云 閱讀:181 作者:柒染 欄目:云計算

本篇文章為大家展示了如何理解Go里面的sync.Map,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。

基礎(chǔ)筑基

在大多數(shù)語言中原始map都不是一個線程安全的數(shù)據(jù)結(jié)構(gòu),那如果要在多個線程或者goroutine中對線程進(jìn)行更改就需要加鎖,除了加1個大鎖,不同的語言還有不同的優(yōu)化方式, 像在java和go這種語言其實都采用的是鏈表法來進(jìn)行map的實現(xiàn)。

并發(fā)安全的map實現(xiàn)的三種方式

在go語言中實現(xiàn)多個goroutine并發(fā)安全訪問修改的map的方式,主要有如下三種:

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

上面三種實現(xiàn)具體的性能差異可能還要針對不同的具體的業(yè)務(wù)場景和平臺、數(shù)據(jù)量等因此來進(jìn)行綜合的測試,源碼的學(xué)習(xí)更多的是了解其實現(xiàn)細(xì)節(jié),以便在出現(xiàn)性能瓶頸的時候可以進(jìn)行分析,找出解決解決方案

map的容量問題

如何理解Go里面的sync.Map 在Mutex和RWMutex實現(xiàn)的并發(fā)安全的map中map隨著時間和元素數(shù)量的增加、刪除,容量會不斷的遞增,在某些情況下比如在某個時間點頻繁的進(jìn)行大量數(shù)據(jù)的增加,然后又大量的刪除,其map的容量并不會隨著元素的刪除而縮小,而在sync.Map中,當(dāng)進(jìn)行元素從dirty進(jìn)行提升到read map的時候會進(jìn)行重建,可能會縮容

無鎖讀與讀寫分離

如何理解Go里面的sync.Map

讀寫分離

并發(fā)訪問map讀的主要問題其實是在擴(kuò)容的時候,可能會導(dǎo)致元素被hash到其他的地址,那如果我的讀的map不會進(jìn)行擴(kuò)容操作,就可以進(jìn)行并發(fā)安全的訪問了,而sync.map里面正是采用了這種方式,對增加元素通過dirty來進(jìn)行保存

無鎖讀

通過read只讀和dirty寫map將操作分離,其實就只需要通過原子指令對read map來進(jìn)行讀操作而不需要加鎖了,從而提高讀的性能

寫加鎖與延遲提升

如何理解Go里面的sync.Map

寫加鎖

上面提到增加元素操作可能會先增加大dirty寫map中,那針對多個goroutine同時寫,其實就需要進(jìn)行Mutex加鎖了

延遲提升

上面提到了read只讀map和dirty寫map, 那就會有個問題,默認(rèn)增加元素都放在dirty中,那后續(xù)訪問新的元素如果都通過 mutex加鎖,那read只讀map就失去意義,sync.Map中采用一直延遲提升的策略,進(jìn)行批量將當(dāng)前map中的所有元素都提升到read只讀map中從而為后續(xù)的讀訪問提供無鎖支持

指針與惰性刪除

如何理解Go里面的sync.Map

map里面的指針

map里面存儲數(shù)據(jù)都會涉及到一個問題就是存儲值還是指針,存儲值可以讓 map作為一個大的的對象,減輕垃圾回收的壓力(避免掃描所有小對象),而存儲指針可以減少內(nèi)存利用,而sync.Map中其實采用了指針結(jié)合惰性刪除的方式,來進(jìn)行 map的value的存儲

惰性刪除

惰性刪除是并發(fā)設(shè)計中一中常見的設(shè)計,比如刪除某個個鏈表元素,如果要刪除則需要修改前后元素的指針,而采用惰性刪除,則通常只需要給某個標(biāo)志位設(shè)定為刪除,然后在后續(xù)修改中再進(jìn)行操作,sync.Map中也采用這種方式,通過給指針指向某個標(biāo)識刪除的指針,從而實現(xiàn)惰性刪除

源碼分析

數(shù)據(jù)結(jié)構(gòu)分析

Map

type Map struct {
    mu Mutex
    // read是一個readOnly的指針,里面包含了一個map結(jié)構(gòu),就是我們說的只讀map對該map的元素的訪問
    // 不需要加鎖,只需要通過atomic加載最新的指針即可
    read atomic.Value // readOnly

    // dirty包含部分map的鍵值對,如果要訪問需要進(jìn)行mutex獲取
    // 最終dirty中的元素會被全部提升到read里面的map中
    dirty map[interface{}]*entry

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

readOnly

只讀map,對該map元素的訪問不需要加鎖,但是該map也不會進(jìn)行元素的增加,元素會被先添加到dirty中然后后續(xù)再轉(zhuǎn)移到read只讀map中,通過atomic原子操作不需要進(jìn)行鎖操作

type readOnly struct {
    // m包含所有只讀數(shù)據(jù),不會進(jìn)行任何的數(shù)據(jù)增加和刪除操作
    // 但是可以修改entry的指針因為這個不會導(dǎo)致map的元素移動
    m       map[interface{}]*entry
    // 標(biāo)志位,如果為true則表明當(dāng)前read只讀map的數(shù)據(jù)不完整,dirty map中包含部分?jǐn)?shù)據(jù)
    amended bool 
}

entry

entry是sync.Map中值得指針,如果當(dāng)p指針指向expunged這個指針的時候,則表明該元素被刪除,但不會立即從map中刪除,如果在未刪除之前又重新賦值則會重用該元素

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

數(shù)據(jù)的存儲

如何理解Go里面的sync.Map

2.2.1 元素存在只讀map

元素如果存儲在只讀map中,則只需要獲取entry元素,然后修改其p的指針指向新的元素就可以了,因為是原地操作所以map不會發(fā)生變化

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

元素存在進(jìn)行變更后的只讀map中

如果此時發(fā)現(xiàn)元素存在只讀 map中,則證明之前有操作觸發(fā)了從dirty到read map的遷移,如果此時發(fā)現(xiàn)存在則修改指針即可

    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之前已經(jīng)被刪除,則這個地方會將key從進(jìn)行nil覆蓋之前已經(jīng)刪除的指針
            // 然后將它加入到dirty中
            m.dirty[key] = e
        }
        // 調(diào)用atomic進(jìn)行value存儲
        e.storeLocked(&value)
    }

元素存在dirty map中

如果元素存在dirty中其實同read map邏輯一樣,只需要修改對應(yīng)元素的指針即可

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

元素之前不存在

如果元素之前不存在當(dāng)前Map中則需要先將其存儲在dirty map中,同時將amended標(biāo)識為true,即當(dāng)前read中的數(shù)據(jù)不全,有一部分?jǐn)?shù)據(jù)存儲在dirty中

        // 如果當(dāng)前不是在修正狀態(tài)
        if !read.amended {               
            // 新加入的key會先被添加到dirty map中, 并進(jìn)行read標(biāo)記為不完整
            // 如果dirty為空則將read中的所有沒有被刪除的數(shù)據(jù)都遷移到dirty中
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)

數(shù)據(jù)遷移

read map到dirty map的遷移

如何理解Go里面的sync.Map 在剛初始化和將所有元素遷移到read中后,dirty默認(rèn)都是nil元素,而此時如果有新的元素增加,則需要先將read map中的所有未刪除數(shù)據(jù)先遷移到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的遷移

如何理解Go里面的sync.Map 當(dāng)持續(xù)的從read訪問穿透到dirty中后,就會觸發(fā)一次從dirty到read的遷移,這也意味著如果我們的元素讀寫比差比較小,其實就會導(dǎo)致頻繁的遷移操作,性能其實可能并不如rwmutex等實現(xiàn)

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
}

數(shù)據(jù)加載

如何理解Go里面的sync.Map

只讀無鎖

Load數(shù)據(jù)的時候回先從read中獲取,如果此時發(fā)現(xiàn)元素,則直接返回即可

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

加鎖讀取read和dirty

加鎖后會嘗試從read和dirty中讀取,同時進(jìn)行misses計數(shù)器的遞增,如果滿足遷移條件則會進(jìn)行數(shù)據(jù)遷移

       read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 這里將采取緩慢遷移的策略
            // 只有當(dāng)misses計數(shù)==len(m.dirty)的時候,才會將dirty里面的數(shù)據(jù)全部晉升到read中
            m.missLocked()
        }

數(shù)據(jù)刪除

如何理解Go里面的sync.Map 數(shù)據(jù)刪除則分為兩個過程,如果數(shù)據(jù)在read中,則就直接修改entry的標(biāo)志位指向刪除的指針即可,如果當(dāng)前read中數(shù)據(jù)不全,則需要進(jìn)行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重復(fù)讀

因為mutex互斥的是所有操作,包括dirty map的修改、數(shù)據(jù)的遷移、刪除,如果在進(jìn)行m.lock的時候,已經(jīng)有一個提升dirty到read操作在進(jìn)行,則執(zhí)行完成后dirty實際上是沒有數(shù)據(jù)的,所以此時要再次進(jìn)行read的重復(fù)讀 

上述內(nèi)容就是如何理解Go里面的sync.Map,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI