您好,登錄后才能下訂單哦!
本篇文章為大家展示了如何理解Go里面的sync.Map,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
在大多數語言中原始map都不是一個線程安全的數據結構,那如果要在多個線程或者goroutine中對線程進行更改就需要加鎖,除了加1個大鎖,不同的語言還有不同的優化方式, 像在java和go這種語言其實都采用的是鏈表法來進行map的實現。
在go語言中實現多個goroutine并發安全訪問修改的map的方式,主要有如下三種:
實現方式 | 原理 | 適用場景 |
---|---|---|
map+Mutex | 通過Mutex互斥鎖來實現多個goroutine對map的串行化訪問 | 讀寫都需要通過Mutex加鎖和釋放鎖,適用于讀寫比接近的場景 |
map+RWMutex | 通過RWMutex來實現對map的讀寫進行讀寫鎖分離加鎖,從而實現讀的并發性能提高 | 同Mutex相比適用于讀多寫少的場景 |
sync.Map | 底層通分離讀寫map和原子指令來實現讀的近似無鎖,并通過延遲更新的方式來保證讀的無鎖化 | 讀多修改少,元素增加刪除頻率不高的情況,在大多數情況下替代上述兩種實現 |
上面三種實現具體的性能差異可能還要針對不同的具體的業務場景和平臺、數據量等因此來進行綜合的測試,源碼的學習更多的是了解其實現細節,以便在出現性能瓶頸的時候可以進行分析,找出解決解決方案
在Mutex和RWMutex實現的并發安全的map中map隨著時間和元素數量的增加、刪除,容量會不斷的遞增,在某些情況下比如在某個時間點頻繁的進行大量數據的增加,然后又大量的刪除,其map的容量并不會隨著元素的刪除而縮小,而在sync.Map中,當進行元素從dirty進行提升到read map的時候會進行重建,可能會縮容
并發訪問map讀的主要問題其實是在擴容的時候,可能會導致元素被hash到其他的地址,那如果我的讀的map不會進行擴容操作,就可以進行并發安全的訪問了,而sync.map里面正是采用了這種方式,對增加元素通過dirty來進行保存
通過read只讀和dirty寫map將操作分離,其實就只需要通過原子指令對read map來進行讀操作而不需要加鎖了,從而提高讀的性能
上面提到增加元素操作可能會先增加大dirty寫map中,那針對多個goroutine同時寫,其實就需要進行Mutex加鎖了
上面提到了read只讀map和dirty寫map, 那就會有個問題,默認增加元素都放在dirty中,那后續訪問新的元素如果都通過 mutex加鎖,那read只讀map就失去意義,sync.Map中采用一直延遲提升的策略,進行批量將當前map中的所有元素都提升到read只讀map中從而為后續的讀訪問提供無鎖支持
map里面存儲數據都會涉及到一個問題就是存儲值還是指針,存儲值可以讓 map作為一個大的的對象,減輕垃圾回收的壓力(避免掃描所有小對象),而存儲指針可以減少內存利用,而sync.Map中其實采用了指針結合惰性刪除的方式,來進行 map的value的存儲
惰性刪除是并發設計中一中常見的設計,比如刪除某個個鏈表元素,如果要刪除則需要修改前后元素的指針,而采用惰性刪除,則通常只需要給某個標志位設定為刪除,然后在后續修改中再進行操作,sync.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 }
只讀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是sync.Map中值得指針,如果當p指針指向expunged這個指針的時候,則表明該元素被刪除,但不會立即從map中刪除,如果在未刪除之前又重新賦值則會重用該元素
type entry struct { // 指向元素實際值得指針 p unsafe.Pointer // *interface{} }
元素如果存儲在只讀map中,則只需要獲取entry元素,然后修改其p的指針指向新的元素就可以了,因為是原地操作所以map不會發生變化
read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { return }
如果此時發現元素存在只讀 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中其實同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中后,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 } } }
當持續的從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 }
Load數據的時候回先從read中獲取,如果此時發現元素,則直接返回即可
read, _ := m.read.Load().(readOnly) e, ok := read.m[key]
加鎖后會嘗試從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() }
數據刪除則分為兩個過程,如果數據在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互斥的是所有操作,包括dirty map的修改、數據的遷移、刪除,如果在進行m.lock的時候,已經有一個提升dirty到read操作在進行,則執行完成后dirty實際上是沒有數據的,所以此時要再次進行read的重復讀
上述內容就是如何理解Go里面的sync.Map,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。