Golang?sync.Map底層實(shí)現(xiàn)場景示例詳解
引言
Go中普通的map是非線程安全的,想要線程安全的訪問一個(gè)map,有兩種方式一種是map+mutex另一種就是原生的sync.Map,這篇文章會詳細(xì)的介紹sync.Map底層是如何實(shí)現(xiàn)的,以及一些常用的場景。
如何保證線程安全?
sync.Map的數(shù)據(jù)結(jié)構(gòu)如下:
type Map struct { mu Mutex // 鎖,保證寫操作及dirty晉升為read的線程安全 read atomic.Value // readOnly 只讀map dirty map[any]*entry // 臟map,當(dāng)內(nèi)部有數(shù)據(jù)時(shí)就一定包含read中的數(shù)據(jù) misses int // read未命中次數(shù),當(dāng)達(dá)到一定次數(shù)時(shí)會觸發(fā)dirty中的護(hù)具晉升到read }
如果只看這個(gè)結(jié)構(gòu)我們可能會有以下幾個(gè)疑問:
- sync.Map中也用了mutex那和map+mutex的實(shí)現(xiàn)方式不就一樣了嗎?
- misses做什么用的?
- read的類型是一個(gè)
atomic.Value
而dirty是map[any]*entry
,為什么不同?
sync.Map中也用了mutex那和map+mutex的實(shí)現(xiàn)方式不就一樣了嗎?
- 在本質(zhì)上都是通過map+mutex的實(shí)現(xiàn)方式來實(shí)現(xiàn)的
- sync.Map通過增加read map,降低在進(jìn)行讀取操作時(shí)的加鎖概率,增加讀取的性能。
misses做什么用的?
- misses是用于標(biāo)記read中未命中次數(shù)的
- 當(dāng)misses達(dá)到一定值時(shí)會觸發(fā)dirty的晉升(晉升為read)
具體源碼如下:
// 當(dāng)執(zhí)行Load操作時(shí)沒能在read中命中key,則進(jìn)行一次miss記錄 func (m *Map) missLocked() { // 1.miss計(jì)數(shù)加1 m.misses++ // 2.判斷dirty是否滿足晉升條件 if m.misses < len(m.dirty) { // 2.1不滿足直接返回 return } // 3.將dirty中的數(shù)據(jù)轉(zhuǎn)存到read的m中,舊的read中的數(shù)據(jù)被拋棄 m.read.Store(readOnly{m: m.dirty}) // 4.清空dirty m.dirty = nil // 5.重置miss計(jì)數(shù) m.misses = 0 }
從代碼中可以看到:
- 當(dāng)misses值大于等于dirty中數(shù)據(jù)個(gè)數(shù)的時(shí)候會觸發(fā)dirty的晉升
- 在dirty晉升時(shí),直直接把read重置成了一個(gè)新生成的readOnly,其中m為新的dirty,amended為默認(rèn)值false,保證每次觸發(fā)晉升都自動將amended設(shè)置為了false
- 在dirty晉升時(shí),并沒有觸發(fā)數(shù)據(jù)的拷貝
read的類型是一個(gè)atomic.Value
而dirty是map[any]*entry
,為什么不同?
type readOnly struct { m map[any]*entry // read map中的數(shù)據(jù) amended bool // 標(biāo)記dirty map中是否有read中沒有的key,如果有,則此值為true } type entry struct { p unsafe.Pointer // *interface{} 一個(gè)指向具體數(shù)據(jù)的指針 }
- read的類型底層是存儲的readOnly類型,而readOnly類型只是在
map[any]*entry
的基礎(chǔ)上增加了一個(gè)amended
標(biāo)記 - 如果amended為false,則代表dirty中沒有read中沒有的數(shù)據(jù),此時(shí)可以避免一次dirty操作(會加鎖),從而降低無意義的加鎖。
- read被聲明為
atomic.Value
類型是為了滿足在無鎖的情況下多個(gè)Goroutine同時(shí)讀取read時(shí)的數(shù)據(jù)一致性
sync.Map適用于那些場景?
sync.Map更適合讀多寫少的場景,而當(dāng)map需要頻繁寫入的時(shí)候,map+mutex的方案通過控制鎖的力度可以達(dá)到比sync.Map更好的性能。
sync.Map不支持遍歷操作,因?yàn)樽x寫分離的設(shè)計(jì)使得在遍歷過程中可能存在一些未完成的修改操作,導(dǎo)致遍歷結(jié)果不確定。
為什么sync.Map適合讀多寫少的場景?
sync.Map的讀取方法為Load
方法,具體的源碼實(shí)現(xiàn)如下:
func (m *Map) Load(key any) (value any, ok bool) { // 1.將read中的數(shù)據(jù)強(qiáng)轉(zhuǎn)為readOnly read, _ := m.read.Load().(readOnly) // 2.從read中查詢key,檢查數(shù)據(jù)是否存在 e, ok := read.m[key] // 3.如果read中不存在,且amended標(biāo)記顯示dirty中存在read中沒有的key,則去dirty中查詢 if !ok && read.amended { // 3.1開始操作dirty,需要加鎖保證線程安全 m.mu.Lock() // 3.2 重新從read中檢查一次,避免在Lock執(zhí)行前dirty中的數(shù)據(jù)觸發(fā)了晉升到read的操作 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 3.3 同3 if !ok && read.amended { // 3.4 從dirty中查詢 e, ok = m.dirty[key] // 3.5 無論是否從dirty中查詢到數(shù)據(jù),都相當(dāng)于從read中miss了,需要更新miss計(jì)數(shù)(更新計(jì)數(shù)可能會觸發(fā)dirty數(shù)據(jù)的晉升) m.missLocked() } // 3.5 操作完成解鎖 m.mu.Unlock() } // 4.檢測結(jié)果,ok為false代表沒有查詢到數(shù)據(jù) // ok為true分為兩種情況:1.從read中查詢到了數(shù)據(jù),read命中;2.從dirty中查詢到了數(shù)據(jù) // ok為false分為兩種情況: // 1.read沒有命中,但read.amended為false // 2.read沒有命中,read.amended為true,但dirty中也不存在 if !ok { return nil, false } // 返回查詢到的數(shù)據(jù)(這個(gè)值也并不一定是真的存在,需要根據(jù)p確定是一個(gè)正常的值還是一個(gè)nil) return e.load() } // 當(dāng)從read或者dirty中獲取到一個(gè)key的值的指針時(shí),需要去加載對應(yīng)指針的值 func (e *entry) load() (value any, ok bool) { // 1.院子操作獲取對應(yīng)地址的值 p := atomic.LoadPointer(&e.p) // 2.如果值已經(jīng)不存在或者標(biāo)記為被刪除則返回nil,false if p == nil || p == expunged { return nil, false } // 3.返回具體的值,true return *(*any)(p), true }
每次讀取數(shù)據(jù)時(shí),優(yōu)先從read中讀取,且read中數(shù)據(jù)的讀取不需要進(jìn)行加鎖操作;當(dāng)read中未命中且amended標(biāo)記顯示dirty中存在read中沒有的數(shù)據(jù)時(shí),才進(jìn)行dirty查詢,并加鎖。在讀多寫少的情況下,大多數(shù)時(shí)候數(shù)據(jù)都在read中所以可以避免加鎖,以此來提高并發(fā)讀的性能。
sync.Map的寫操作方法為Store
方法
func (m *Map) Store(key, value any) { // 1.從read中查詢數(shù)據(jù)是否已經(jīng)存在,如果存在則嘗試修改 read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { // 1.1 read中存在數(shù)據(jù),且更新完成,直接返回 return } //2.read中沒有,準(zhǔn)備操作dirty,為保證線程安全加鎖 m.mu.Lock() read, _ = m.read.Load().(readOnly) if e, ok := read.m[key]; ok { // 3.如果在read中查詢到數(shù)據(jù),則檢查是否已經(jīng)被標(biāo)記為刪除, if e.unexpungeLocked() { // 3.1 如果被標(biāo)記為刪除需要清空標(biāo)記并加入到dirty中 m.dirty[key] = e } // 3.2 更新entry的值 e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { // 4 如果存在于dirty中,則直接更新entry的值 e.storeLocked(&value) } else { // 5 如果之前這個(gè)key不存在,則將新的key-value加入到dirty中 if !read.amended { // 5.1 如果read.amended標(biāo)記顯示之前dirty中不存在read中沒有的key,則重置dirty,并標(biāo)amended為true // 5.1.1 將read中所有未被標(biāo)記為刪除的entry重新加入到dirty中 m.dirtyLocked() // 5.1.2 更新read.amended標(biāo)記 m.read.Store(readOnly{m: read.m, amended: true}) } // 5.2 將新的key-value加入到dirty中 m.dirty[key] = newEntry(value) } m.mu.Unlock() } // 判斷entry是否被標(biāo)記為刪除,如果是將其修改為nil func (e *entry) unexpungeLocked() (wasExpunged bool) { return atomic.CompareAndSwapPointer(&e.p, expunged, nil) }
寫操作分為以下幾種情況:
- 數(shù)據(jù)在read中
- 數(shù)據(jù)在read中但已經(jīng)被標(biāo)記為刪除
- 數(shù)據(jù)在dirty中
- 一個(gè)全新的數(shù)據(jù)
當(dāng)數(shù)據(jù)在read中時(shí),Store會嘗試通過原子操作修改數(shù)據(jù),如果原子操作成功,則相當(dāng)于數(shù)據(jù)更新完成;
具體的代碼如下:
func (e *entry) tryStore(i *any) bool { for { // 1.獲取entry具體的值 p := atomic.LoadPointer(&e.p) // 2.如果數(shù)據(jù)已經(jīng)被標(biāo)記刪除,則返回false if p == expunged { return false } // 3.更新當(dāng)前值,并返回true if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { return true } } }
當(dāng)數(shù)據(jù)在read中已經(jīng)被標(biāo)記為刪除,此時(shí)需要重新將entry加入到dirty中,并更新值(這里本質(zhì)上增加了一個(gè)新的entry只是服用了之前entry的地址空間)
當(dāng)數(shù)據(jù)在dirty中時(shí),則直接通過原子操作更新entry的指針;
當(dāng)數(shù)據(jù)是一個(gè)新數(shù)據(jù)時(shí),會創(chuàng)建一個(gè)新的entry加入到dirty中,并且如果是dirty中的第一個(gè)數(shù)據(jù)則會執(zhí)行dirtyLocked
方法,將read中當(dāng)前的數(shù)據(jù)(未標(biāo)記刪除的)加入到dirty中。
dirtyLocked的具體實(shí)現(xiàn)如下:
func (m *Map) dirtyLocked() { //1 dirty之前是nil的情況才可以進(jìn)行重置操作 if m.dirty != nil { return } // 2 獲取read中的數(shù)據(jù) read, _ := m.read.Load().(readOnly) // 3 初始化dirty m.dirty = make(map[any]*entry, len(read.m)) // 4 遍歷read for k, e := range read.m { // 4.1 將非nil且未被標(biāo)記為刪除的對象加入到dirty中 if !e.tryExpungeLocked() { m.dirty[k] = e } } } // 判斷entry是否被標(biāo)記為刪除,如果entry的值為nil,則將其標(biāo)記為刪除 func (e *entry) tryExpungeLocked() (isExpunged bool) { // 1 獲取entry的值 p := atomic.LoadPointer(&e.p) for p == nil { // 2 如果當(dāng)前entry的值為空,則嘗試將此key標(biāo)記為刪除 if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } // 3 判斷p是否為被標(biāo)記為刪除 return p == expunged }
通過上面的分析我們可以發(fā)現(xiàn)當(dāng)寫操作頻繁時(shí)存在以下幾個(gè)問題
- dirty中存在大量數(shù)據(jù),而read的查詢會大概率無法命中,從而導(dǎo)致查詢需要查詢r(jià)ead和dirty兩個(gè)map且有額外的冗余操作,所以讀性能被大大降低
- 頻繁的無法命中導(dǎo)致dirty數(shù)據(jù)的晉升,雖然晉升時(shí)只是進(jìn)行指針切換及dirty的清空,但每次晉升后的第一次寫入都會導(dǎo)致dirty對read進(jìn)行拷貝,大大降低性能。
- 每次寫操作為了因?yàn)椴淮_定數(shù)據(jù)是在read還是dirty或者新數(shù)據(jù)需要進(jìn)行額外的檢查和操作
- dirty中和read中在某些情況下存在數(shù)據(jù)重復(fù),內(nèi)存占用會高一些
綜上,在寫操作比較頻繁的時(shí)候,sync.Map的各方面性能都大大降低;而對于一些只有極少寫操作的數(shù)據(jù)(比如:只在服務(wù)器啟動時(shí)加載一次的表格數(shù)據(jù)),sync.Map可以提高并發(fā)操作的性能。
如何刪除數(shù)據(jù)
在上面的dirtyLoacked方法中我們看到當(dāng)初始化dirty后,會遍歷read中的數(shù)據(jù),將非nil且未被標(biāo)記為刪除的對象加入到dirty中。由此可以看出read中的數(shù)據(jù)在刪除時(shí)并不會立刻刪除只是將對象標(biāo)記為nil或者expunged。
具體代碼如下:(Delete方法本質(zhì)上就是執(zhí)行的LoadAndDelete)
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) { read, _ := m.read.Load().(readOnly) // 1 從read中查詢數(shù)據(jù) e, ok := read.m[key] // 2 如果read中不存在,且amended標(biāo)記顯示dirty中存在read中沒有的key,則去dirty中查詢 if !ok && read.amended { // 3.1開始操作dirty,需要加鎖保證線程安全 m.mu.Lock() read, _ = m.read.Load().(readOnly) // 3.2 重新從read中檢查一次,避免在Lock執(zhí)行前dirty中的數(shù)據(jù)觸發(fā)了晉升到read的操作 e, ok = read.m[key] if !ok && read.amended { // 3.3 從dirty中查詢 e, ok = m.dirty[key] // 3.4 從dirty中刪除 delete(m.dirty, key) // 3.5 無論是否從dirty中查詢到數(shù)據(jù),都相當(dāng)于從read中miss了,需要更新miss計(jì)數(shù)(滿足條件后會觸發(fā)dirty晉升) m.missLocked() } m.mu.Unlock() } // 4 和load一樣,這里ok為true可能是在read中讀取到數(shù)據(jù)或者dirty中讀取到數(shù)據(jù), // dirty中的話雖然已經(jīng)刪除但需要清空entry中的指針p if ok { // 5 標(biāo)記刪除 return e.delete() } return nil, false }
如上述代碼第5
部分所示,無論entry存在哪里,最終都需要將entry標(biāo)記為刪除。如果存在read中會在dirty初始化時(shí)不被加入到dirty中,當(dāng)dirty再次晉升時(shí)read中的數(shù)據(jù)也就被拋棄了。如果存在dirty中則直接清空了數(shù)據(jù)并標(biāo)記entry被刪除。
sync.Map的Range方法
sync.Map并不支持遍歷,但卻提供了一個(gè)Range方法,此方法并不是和range
關(guān)鍵字一樣對map的遍歷。
Range方法的具體作用:
- 遍歷所有read中的元素,對其中的每個(gè)元素執(zhí)行函數(shù)f
- 如果當(dāng)任何一個(gè)元素作為參數(shù)執(zhí)行函數(shù)f返回false,則立刻中斷遍歷
雖然在執(zhí)行初始階段Range會將dirty的數(shù)據(jù)晉升一次,但仍然不能保證在執(zhí)行過程中沒有新的數(shù)據(jù),所以Range只是遍歷了最新的read中的數(shù)據(jù),而非全部數(shù)據(jù)。
// 遍歷sync.Map func (m *Map) Range(f func(key, value any) bool) { read, _ := m.read.Load().(readOnly) // 1 如果存在未晉升的數(shù)據(jù),則先進(jìn)行一次dirty數(shù)據(jù)晉升 if read.amended { m.mu.Lock() read, _ = m.read.Load().(readOnly) if read.amended { read = readOnly{m: m.dirty} m.read.Store(read) m.dirty = nil m.misses = 0 } m.mu.Unlock() } // 2 遍歷read中的所有entry,分別執(zhí)行f函數(shù) for k, e := range read.m { v, ok := e.load() if !ok { continue } // 3 當(dāng)某個(gè)entery執(zhí)行f函數(shù)返回false,則中斷遍歷 if !f(k, v) { break } } }
其他
問:什么時(shí)候清除被標(biāo)記刪除的value
答:當(dāng)首次向dirty中存入數(shù)據(jù)時(shí),會觸發(fā)dirty復(fù)制read中的內(nèi)容,此時(shí)再復(fù)制時(shí)只復(fù)制了非nil且未被標(biāo)記刪除的entry,當(dāng)dirty再次晉升時(shí)就覆蓋掉了read中的數(shù)據(jù),實(shí)現(xiàn)被標(biāo)記刪除的entry的刪除。
以上就是Golang sync.Map底層實(shí)現(xiàn)場景示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang sync.Map底層實(shí)現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言模型:string的底層數(shù)據(jù)結(jié)構(gòu)與高效操作詳解
這篇文章主要介紹了Go語言模型:string的底層數(shù)據(jù)結(jié)構(gòu)與高效操作詳解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12使用golang實(shí)現(xiàn)在屏幕上打印進(jìn)度條的操作
這篇文章主要介紹了使用golang實(shí)現(xiàn)在屏幕上打印進(jìn)度條的操作,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03go語言中sort包的實(shí)現(xiàn)方法與應(yīng)用詳解
golang中也實(shí)現(xiàn)了排序算法的包sort包,所以下面這篇文章主要給大家介紹了關(guān)于go語言中sort包的實(shí)現(xiàn)方法與應(yīng)用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11實(shí)時(shí)通信的服務(wù)器推送機(jī)制 EventSource(SSE) 簡介附go實(shí)現(xiàn)示例代碼
EventSource是一種非常有用的 API,適用于許多實(shí)時(shí)應(yīng)用場景,它提供了一種簡單而可靠的方式來建立服務(wù)器推送連接,并實(shí)現(xiàn)實(shí)時(shí)更新和通知,這篇文章主要介紹了實(shí)時(shí)通信的服務(wù)器推送機(jī)制 EventSource(SSE)簡介附go實(shí)現(xiàn)示例,需要的朋友可以參考下2024-03-03