您好,登錄后才能下訂單哦!
本篇文章為大家展示了如何理解Go里面的sync.Map,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
在大多數(shù)語言中原始map都不是一個線程安全的數(shù)據(jù)結(jié)構(gòu),那如果要在多個線程或者goroutine中對線程進(jìn)行更改就需要加鎖,除了加1個大鎖,不同的語言還有不同的優(yōu)化方式, 像在java和go這種語言其實都采用的是鏈表法來進(jìn)行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)行分析,找出解決解決方案
在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)行重建,可能會縮容
并發(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)行讀操作而不需要加鎖了,從而提高讀的性能
上面提到增加元素操作可能會先增加大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ù)的讀訪問提供無鎖支持
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)惰性刪除
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 }
只讀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是sync.Map中值得指針,如果當(dāng)p指針指向expunged這個指針的時候,則表明該元素被刪除,但不會立即從map中刪除,如果在未刪除之前又重新賦值則會重用該元素
type entry struct { // 指向元素實際值得指針 p unsafe.Pointer // *interface{} }
元素如果存儲在只讀map中,則只需要獲取entry元素,然后修改其p的指針指向新的元素就可以了,因為是原地操作所以map不會發(fā)生變化
read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { return }
如果此時發(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中其實同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)
在剛初始化和將所有元素遷移到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 } } }
當(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 }
Load數(shù)據(jù)的時候回先從read中獲取,如果此時發(fā)現(xiàn)元素,則直接返回即可
read, _ := m.read.Load().(readOnly) e, ok := read.m[key]
加鎖后會嘗試從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ù)刪除則分為兩個過程,如果數(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互斥的是所有操作,包括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è)資訊頻道。
免責(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)容。