Golang?sync.Map底層實(shí)現(xiàn)場(chǎng)景示例詳解
引言
Go中普通的map是非線程安全的,想要線程安全的訪問(wèn)一個(gè)map,有兩種方式一種是map+mutex另一種就是原生的sync.Map,這篇文章會(huì)詳細(xì)的介紹sync.Map底層是如何實(shí)現(xiàn)的,以及一些常用的場(chǎng)景。
如何保證線程安全?
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í)會(huì)觸發(fā)dirty中的護(hù)具晉升到read
}如果只看這個(gè)結(jié)構(gòu)我們可能會(huì)有以下幾個(gè)疑問(wèn):
- 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ì)上都是通過(guò)map+mutex的實(shí)現(xiàn)方式來(lái)實(shí)現(xiàn)的
- sync.Map通過(guò)增加read map,降低在進(jìn)行讀取操作時(shí)的加鎖概率,增加讀取的性能。
misses做什么用的?
- misses是用于標(biāo)記read中未命中次數(shù)的
- 當(dāng)misses達(dá)到一定值時(shí)會(huì)觸發(fā)dirty的晉升(晉升為read)
具體源碼如下:
// 當(dāng)執(zhí)行Load操作時(shí)沒(méi)能在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í)候會(huì)觸發(fā)dirty的晉升
- 在dirty晉升時(shí),直直接把read重置成了一個(gè)新生成的readOnly,其中m為新的dirty,amended為默認(rèn)值false,保證每次觸發(fā)晉升都自動(dòng)將amended設(shè)置為了false
- 在dirty晉升時(shí),并沒(méi)有觸發(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中沒(méi)有的key,如果有,則此值為true
}
type entry struct {
p unsafe.Pointer // *interface{} 一個(gè)指向具體數(shù)據(jù)的指針
}- read的類型底層是存儲(chǔ)的readOnly類型,而readOnly類型只是在
map[any]*entry的基礎(chǔ)上增加了一個(gè)amended標(biāo)記 - 如果amended為false,則代表dirty中沒(méi)有read中沒(méi)有的數(shù)據(jù),此時(shí)可以避免一次dirty操作(會(huì)加鎖),從而降低無(wú)意義的加鎖。
- read被聲明為
atomic.Value類型是為了滿足在無(wú)鎖的情況下多個(gè)Goroutine同時(shí)讀取read時(shí)的數(shù)據(jù)一致性
sync.Map適用于那些場(chǎng)景?
sync.Map更適合讀多寫少的場(chǎng)景,而當(dāng)map需要頻繁寫入的時(shí)候,map+mutex的方案通過(guò)控制鎖的力度可以達(dá)到比sync.Map更好的性能。
sync.Map不支持遍歷操作,因?yàn)樽x寫分離的設(shè)計(jì)使得在遍歷過(guò)程中可能存在一些未完成的修改操作,導(dǎo)致遍歷結(jié)果不確定。
為什么sync.Map適合讀多寫少的場(chǎng)景?
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中沒(méi)有的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 無(wú)論是否從dirty中查詢到數(shù)據(jù),都相當(dāng)于從read中miss了,需要更新miss計(jì)數(shù)(更新計(jì)數(shù)可能會(huì)觸發(fā)dirty數(shù)據(jù)的晉升)
m.missLocked()
}
// 3.5 操作完成解鎖
m.mu.Unlock()
}
// 4.檢測(cè)結(jié)果,ok為false代表沒(méi)有查詢到數(shù)據(jù)
// ok為true分為兩種情況:1.從read中查詢到了數(shù)據(jù),read命中;2.從dirty中查詢到了數(shù)據(jù)
// ok為false分為兩種情況:
// 1.read沒(méi)有命中,但read.amended為false
// 2.read沒(méi)有命中,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í),需要去加載對(duì)應(yīng)指針的值
func (e *entry) load() (value any, ok bool) {
// 1.院子操作獲取對(duì)應(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中沒(méi)有的數(shù)據(jù)時(shí),才進(jìn)行dirty查詢,并加鎖。在讀多寫少的情況下,大多數(shù)時(shí)候數(shù)據(jù)都在read中所以可以避免加鎖,以此來(lái)提高并發(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中沒(méi)有,準(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中沒(méi)有的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會(huì)嘗試通過(guò)原子操作修改數(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í),則直接通過(guò)原子操作更新entry的指針;
當(dāng)數(shù)據(jù)是一個(gè)新數(shù)據(jù)時(shí),會(huì)創(chuàng)建一個(gè)新的entry加入到dirty中,并且如果是dirty中的第一個(gè)數(shù)據(jù)則會(huì)執(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)記為刪除的對(duì)象加入到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
}通過(guò)上面的分析我們可以發(fā)現(xiàn)當(dāng)寫操作頻繁時(shí)存在以下幾個(gè)問(wèn)題
- dirty中存在大量數(shù)據(jù),而read的查詢會(huì)大概率無(wú)法命中,從而導(dǎo)致查詢需要查詢r(jià)ead和dirty兩個(gè)map且有額外的冗余操作,所以讀性能被大大降低
- 頻繁的無(wú)法命中導(dǎo)致dirty數(shù)據(jù)的晉升,雖然晉升時(shí)只是進(jìn)行指針切換及dirty的清空,但每次晉升后的第一次寫入都會(huì)導(dǎo)致dirty對(duì)read進(jìn)行拷貝,大大降低性能。
- 每次寫操作為了因?yàn)椴淮_定數(shù)據(jù)是在read還是dirty或者新數(shù)據(jù)需要進(jìn)行額外的檢查和操作
- dirty中和read中在某些情況下存在數(shù)據(jù)重復(fù),內(nèi)存占用會(huì)高一些
綜上,在寫操作比較頻繁的時(shí)候,sync.Map的各方面性能都大大降低;而對(duì)于一些只有極少寫操作的數(shù)據(jù)(比如:只在服務(wù)器啟動(dòng)時(shí)加載一次的表格數(shù)據(jù)),sync.Map可以提高并發(fā)操作的性能。
如何刪除數(shù)據(jù)
在上面的dirtyLoacked方法中我們看到當(dāng)初始化dirty后,會(huì)遍歷read中的數(shù)據(jù),將非nil且未被標(biāo)記為刪除的對(duì)象加入到dirty中。由此可以看出read中的數(shù)據(jù)在刪除時(shí)并不會(huì)立刻刪除只是將對(duì)象標(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中沒(méi)有的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 無(wú)論是否從dirty中查詢到數(shù)據(jù),都相當(dāng)于從read中miss了,需要更新miss計(jì)數(shù)(滿足條件后會(huì)觸發(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部分所示,無(wú)論entry存在哪里,最終都需要將entry標(biāo)記為刪除。如果存在read中會(huì)在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)鍵字一樣對(duì)map的遍歷。
Range方法的具體作用:
- 遍歷所有read中的元素,對(duì)其中的每個(gè)元素執(zhí)行函數(shù)f
- 如果當(dāng)任何一個(gè)元素作為參數(shù)執(zhí)行函數(shù)f返回false,則立刻中斷遍歷
雖然在執(zhí)行初始階段Range會(huì)將dirty的數(shù)據(jù)晉升一次,但仍然不能保證在執(zhí)行過(guò)程中沒(méi)有新的數(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
}
}
}其他
問(wèn):什么時(shí)候清除被標(biāo)記刪除的value
答:當(dāng)首次向dirty中存入數(shù)據(jù)時(shí),會(huì)觸發(fā)dirty復(fù)制read中的內(nèi)容,此時(shí)再?gòu)?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)場(chǎng)景示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang sync.Map底層實(shí)現(xiàn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang實(shí)現(xiàn)按行讀取文件的方法小結(jié)
按行讀取文件相較于一次性載入,有著很多優(yōu)勢(shì),如內(nèi)存效率高、處理速度快、實(shí)時(shí)性高等,本文主要介紹了Golang按行讀取文件的相關(guān)方法,希望對(duì)大家有所幫助2024-02-02
Go使用github.com/google/uuid生成唯一ID的操作方案
這篇文章介紹了Go語(yǔ)言中常用的UUID庫(kù)github.com/google/uuid,詳細(xì)講解了UUID的定義、特點(diǎn)、生成方法、轉(zhuǎn)換、解析、應(yīng)用場(chǎng)景以及性能情況,需要的朋友可以參考下2026-03-03
使用golang引入外部包的三種方式:go get, go module, ve
這篇文章主要介紹了使用golang引入外部包的三種方式:go get, go module, vendor目錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Golang使用Gin框架實(shí)現(xiàn)http分塊傳輸
這篇文章主要為大家詳細(xì)介紹了Golang中如何使用Gin框架實(shí)現(xiàn)http分塊傳輸功能,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,需要的可以參考一下2023-05-05
Windows上安裝Go并配置環(huán)境變量(圖文步驟)
開始使用Go創(chuàng)建應(yīng)用程序之前,需要設(shè)置開發(fā)環(huán)境,本文主要介紹了Windows上安裝Go并配置環(huán)境變量,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08
Go語(yǔ)言sync.Pool對(duì)象池使用場(chǎng)景基本示例
這篇文章主要為大家介紹了Go語(yǔ)言sync.Pool對(duì)象池使用場(chǎng)景的基本示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12

