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

