Golang sync.Map原理深入分析講解
GO語(yǔ)言內(nèi)置的map
go語(yǔ)言內(nèi)置一個(gè)map數(shù)據(jù)結(jié)構(gòu),使用起來(lái)非常方便,但是它僅支持并發(fā)的讀,不支持并發(fā)的寫,比如下面的代碼:
在main函數(shù)中開(kāi)啟兩個(gè)協(xié)程同時(shí)對(duì)m進(jìn)行并發(fā)讀和并發(fā)寫,程序運(yùn)行之后會(huì)報(bào)錯(cuò):
package main
func main() {
m := make(map[int]int)
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[2] = 2
}
}()
select {}
}

改進(jìn)
既然不可以并發(fā)的寫,我們可以給map加一個(gè)讀寫鎖,這樣就不會(huì)有并發(fā)寫沖突的問(wèn)題了:
import "sync"
func main() {
m := make(map[int]int)
var lock sync.RWMutex
go func() {
for {
lock.RLock()
_ = m[1]
lock.RUnlock()
}
}()
go func() {
for {
lock.Lock()
m[2] = 2
lock.Unlock()
}
}()
select {}
}這種方式的實(shí)現(xiàn)非常簡(jiǎn)潔,但也存在一些問(wèn)題,比如在map的數(shù)據(jù)非常大的情況下,一把鎖會(huì)導(dǎo)致大并發(fā)的客戶端共爭(zhēng)一把鎖。
sync.Map
sync.Map是官方在sync包中提供的一種并發(fā)map,使用起來(lái)非常簡(jiǎn)單,和普通map相比,只有遍歷的方式有區(qū)別:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 1. 寫入
m.Store("apple", 1)
m.Store("banana", 2)
// 2. 讀取
price, _ := m.Load("apple")
fmt.Println(price.(int))
// 3. 遍歷
m.Range(func(key, value interface{}) bool {
fruit := key.(string)
price := value.(int)
fmt.Println(fruit, price)
return true
})
// 4. 刪除
m.Delete("apple")
// 5. 讀取或?qū)懭?
m.LoadOrStore("peach", 3)
}
sync.Map是通過(guò) read 和 dirty 兩個(gè)字段將讀寫分離,讀的數(shù)據(jù)存在只讀字段 read 上,將最新寫入的數(shù)據(jù)則存在 dirty 字段上。
讀取時(shí)會(huì)先查詢 read,不存在再查詢 dirty,寫入時(shí)則只寫入 dirty。
讀取 read 并不需要加鎖,而讀或?qū)?dirty 都需要加鎖,另外有 misses 字段來(lái)統(tǒng)計(jì) read 被穿透的次數(shù)(被穿透指需要讀 dirty 的情況),超過(guò)一定次數(shù)則將 dirty 數(shù)據(jù)同步到 read 上,對(duì)于刪除數(shù)據(jù)則直接通過(guò)標(biāo)記來(lái)延遲刪除。
在map + 鎖的基礎(chǔ)上,它有著幾個(gè)優(yōu)化點(diǎn):
- 空間換時(shí)間。 通過(guò)冗余的兩個(gè)數(shù)據(jù)結(jié)構(gòu)(read、dirty),實(shí)現(xiàn)加鎖對(duì)性能的影響。
- 使用只讀數(shù)據(jù)(read),避免讀寫沖突。
- 動(dòng)態(tài)調(diào)整,miss次數(shù)多了之后,將dirty數(shù)據(jù)提升為read。
- double-checking。
- 延遲刪除。 刪除一個(gè)鍵值只是打標(biāo)記,只有在提升dirty的時(shí)候才清理刪除的數(shù)據(jù)。
- 優(yōu)先從read讀取、更新、刪除,因?yàn)閷?duì)read的讀取不需要鎖。
sync.Map原理分析
sync.Map的結(jié)構(gòu)
sync.Map的實(shí)現(xiàn)在src/sync/map.go中,首先來(lái)看Map結(jié)構(gòu)體:
type Map struct {
// 當(dāng)涉及到臟數(shù)據(jù)(dirty)操作時(shí)候,需要使用這個(gè)鎖
mu Mutex
// read是一個(gè)只讀數(shù)據(jù)結(jié)構(gòu),包含一個(gè)map結(jié)構(gòu),
// 讀不需要加鎖,只需要通過(guò) atomic 加載最新的指正即可
read atomic.Value // readOnly
// dirty 包含部分map的鍵值對(duì),如果操作需要mutex獲取鎖
// 最后dirty中的元素會(huì)被全部提升到read里的map去
dirty map[interface{}]*entry
// misses是一個(gè)計(jì)數(shù)器,用于記錄read中沒(méi)有的數(shù)據(jù)而在dirty中有的數(shù)據(jù)的數(shù)量。
// 也就是說(shuō)如果read不包含這個(gè)數(shù)據(jù),會(huì)從dirty中讀取,并misses+1
// 當(dāng)misses的數(shù)量等于dirty的長(zhǎng)度,就會(huì)將dirty中的數(shù)據(jù)遷移到read中
misses int
}
上述結(jié)構(gòu)體中的read字段實(shí)際上是一個(gè)包含map的結(jié)構(gòu)體,該結(jié)構(gòu)體中的map是一個(gè)read map,對(duì)該map的訪問(wèn)不需要加鎖,但是增加的元素不會(huì)被添加到這個(gè)map中,元素會(huì)被先增加到dirty中,后續(xù)才會(huì)被遷移到read只讀map中。
readOnly結(jié)構(gòu)體中還有一個(gè)amended字段,該字段是一個(gè)標(biāo)志位,用來(lái)表示read map中的數(shù)據(jù)是否完整。假設(shè)當(dāng)前要查找一個(gè)key,會(huì)先去read map中找,如果沒(méi)有找到,會(huì)判斷amended是否為true,如果為true,說(shuō)明read map的數(shù)據(jù)不完整,需要去dirty map中找。
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
// m包含所有只讀數(shù)據(jù),不會(huì)進(jìn)行任何的數(shù)據(jù)增加和刪除操作
// 但是可以修改entry的指針因?yàn)檫@個(gè)不會(huì)導(dǎo)致map的元素移動(dòng)
m map[interface{}]*entry
// 標(biāo)志位,如果為true則表明當(dāng)前read只讀map的數(shù)據(jù)不完整,dirty map中包含部分?jǐn)?shù)據(jù)
amended bool // true if the dirty map contains some key not in m.
}
entry
readOnly.m和Map.dirty存儲(chǔ)的值類型是*entry,它包含一個(gè)指針p, 指向用戶存儲(chǔ)的value值,結(jié)構(gòu)如下:
type entry struct {
p unsafe.Pointer // *interface{}
}
其中p對(duì)應(yīng)著三種值:
p == nil: 鍵值已經(jīng)被刪除,且m.dirty == nil,這個(gè)時(shí)候dirty在等待read的同步數(shù)據(jù)。p == expunged: 鍵值已經(jīng)被刪除,但m.dirty!=nil且 m.dirty 不存在該鍵值(dirty已經(jīng)得到了read的數(shù)據(jù)同步,原來(lái)為nil的值已經(jīng)被標(biāo)記為了expunged沒(méi)有被同步過(guò)來(lái))。- 除以上情況,則鍵值對(duì)存在,存在于 m.read.m 中,如果 m.dirty!=nil 則也存在于 m.dirty
下面是sync.Map的結(jié)構(gòu)示意圖:

查找
查找元素會(huì)調(diào)用Load函數(shù),該函數(shù)的執(zhí)行流程:
- 首先去read map中找值,不用加鎖,找到了直接返回結(jié)果。
- 如果沒(méi)有找到就判斷
read.amended字段是否為true,true說(shuō)明dirty中有新數(shù)據(jù),應(yīng)該去dirty中查找,開(kāi)始加鎖。 - 加完鎖以后又去read map中查找,因?yàn)樵诩渔i的過(guò)程中,m.dirty可能被提升為m.read。
- 如果二次檢查沒(méi)有找到key,就去
m.dirty中尋找,然后將misses計(jì)數(shù)加一。
// src/sync/map.go
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 首先從只讀ready的map中查找,這時(shí)不需要加鎖
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果沒(méi)有找到,并且read.amended為true,說(shuō)明dirty中有新數(shù)據(jù),從dirty中查找,開(kāi)始加鎖了
if !ok && read.amended {
m.mu.Lock() // 加鎖
// 又在 readonly 中檢查一遍,因?yàn)樵诩渔i的時(shí)候 dirty 的數(shù)據(jù)可能已經(jīng)遷移到了read中
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// read 還沒(méi)有找到,并且dirty中有數(shù)據(jù)
if !ok && read.amended {
e, ok = m.dirty[key] //從 dirty 中查找數(shù)據(jù)
// 不管m.dirty中存不存在,都將misses + 1
// missLocked() 中滿足條件后就會(huì)把m.dirty中數(shù)據(jù)遷移到m.read中
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
misses計(jì)數(shù)
misses計(jì)數(shù)是有上限的,如果misses次數(shù)達(dá)到m.dirty的長(zhǎng)度,就開(kāi)始遷移數(shù)據(jù),程序會(huì)直接將m.dirty提升為m.read,然后將m.dirty置為nil,等到下次插入新數(shù)據(jù)的時(shí)候,程序才會(huì)把read map中的值全部復(fù)制給dirty map。
// src/sync/map.go
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {//misses次數(shù)小于 dirty的長(zhǎng)度,就不遷移數(shù)據(jù),直接返回
return
}
m.read.Store(readOnly{m: m.dirty}) //開(kāi)始遷移數(shù)據(jù)
m.dirty = nil //遷移完dirty就賦值為nil
m.misses = 0 //遷移完 misses歸0
}
新增和更新
新增或者更新元素會(huì)調(diào)用Store函數(shù),該函數(shù)的前面幾個(gè)步驟與Load函數(shù)是一樣的:
- 首先去read map中找值,不用加鎖,找到了直接調(diào)用
tryStore()函數(shù)更新值即可。 - 如果沒(méi)有找到就開(kāi)始對(duì)dirty map加鎖,加完鎖之后再次去read map中找值,如果存在就判斷該key對(duì)應(yīng)的entry有沒(méi)有被標(biāo)記為
unexpunge,如果沒(méi)有被標(biāo)記,就直接調(diào)用storeLocked()函數(shù)更新值即可。 - 如果在read map中進(jìn)行二次檢查還是沒(méi)有找到key,就去dirty map中找,找到了直接調(diào)用
storeLocked()函數(shù)更新值。 - 如果dirty map中也沒(méi)有這個(gè)key,說(shuō)明是新加入的key,首先要將
read.amended標(biāo)記為true,然后將read map中未刪除的值復(fù)制到dirty中,最后向dirty map中加入這個(gè)值。
// src/sync/map.go
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
// 直接在read中查找值,找到了,就嘗試 tryStore() 更新值
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// m.read 中不存在
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() { // 未被標(biāo)記成刪除,前面講到entry數(shù)據(jù)結(jié)構(gòu)時(shí),里面的p值有3種。1.nil 2.expunged,這個(gè)值含義有點(diǎn)復(fù)雜,可以看看前面entry數(shù)據(jù)結(jié)構(gòu) 3.正常值
m.dirty[key] = e // 加入到dirty里
}
e.storeLocked(&value) // 更新值
} else if e, ok := m.dirty[key]; ok { // 存在于 dirty 中,直接更新
e.storeLocked(&value)
} else { // 新的值
if !read.amended { // m.dirty 中沒(méi)有新數(shù)據(jù),增加到 m.dirty 中
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked() // 從 m.read中復(fù)制未刪除的數(shù)據(jù)
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value) //將這個(gè)entry加入到m.dirty中
}
m.mu.Unlock()
}
在Store函數(shù)中我們用到了兩個(gè)用于更新值的函數(shù):tryStore以及storeLocked,tryStore函數(shù)是先判斷p有沒(méi)有被標(biāo)記為expunged(軟刪除),如果被標(biāo)記了就直接返回false,如果沒(méi)有被標(biāo)記,就將p指向的值進(jìn)行更新然后返回true。
storeLocked函數(shù)是直接將p指向的值進(jìn)行更新。
// tryStore stores a value if the entry has not been expunged.
//
// If the entry is expunged, tryStore returns false and leaves the entry
// unchanged.
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
// storeLocked unconditionally stores a value to the entry.
//
// The entry must be known not to be expunged.
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
將read map中的值復(fù)制到dirty map中:
m.dirtyLocked()函數(shù)用于將read map中的值復(fù)制到dirty map中:
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 {
// 判斷值是否被刪除,被標(biāo)記為expunged的值不會(huì)被復(fù)制到read map中
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
// expunged實(shí)際上是一個(gè)指向空接口的unsafe指針
var expunged = unsafe.Pointer(new(interface{}))
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
// 如果p為nil,就會(huì)被標(biāo)記為expunged
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}下面是對(duì)sync.Map進(jìn)行讀寫操作的示意圖,正常讀寫且read map中有數(shù)據(jù),程序只會(huì)訪問(wèn)read map,而不會(huì)去加鎖:

刪除
刪除會(huì)調(diào)用Delete函數(shù),該函數(shù)的步驟如下:
- 首先去read map中找key,找到了就調(diào)用
e.delete()函數(shù)刪除。 - 如果在read map中沒(méi)有找到值就開(kāi)始對(duì)dirty map加鎖,加鎖完畢之后再次去read map中查找,找到了就調(diào)用
e.delete()函數(shù)刪除。 - 如果二次檢查都沒(méi)有找到key(說(shuō)明這個(gè)key是被追加之后,還沒(méi)有提升到read map中就要被刪除),就去dirty map中刪除這個(gè)key。
// src/sync/map.go
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
// 從 m.read 中開(kāi)始查找
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended { // m.read中沒(méi)有找到,并且可能存在于m.dirty中,加鎖查找
m.mu.Lock() // 加鎖
read, _ = m.read.Load().(readOnly) // 再在m.read中查找一次
e, ok = read.m[key]
if !ok && read.amended { //m.read中又沒(méi)找到,amended標(biāo)志位true,說(shuō)明在m.dirty中
delete(m.dirty, key) // 刪除
}
m.mu.Unlock()
}
if ok { // 在 m.read 中就直接刪除
e.delete()
}
}
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
// 已標(biāo)記為刪除
if p == nil || p == expunged {
return false
}
// 原子操作,e.p標(biāo)記為nil,GO的GC會(huì)將對(duì)象自動(dòng)刪除
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
key究竟什么時(shí)候會(huì)被刪除
我們可以發(fā)現(xiàn),如果read map中存在待刪除的key時(shí),程序并不會(huì)去直接刪除這個(gè)key,而是將這個(gè)key對(duì)應(yīng)的p指針指向nil。
在下一次read -> dirty的同步時(shí),指向nil的p指針會(huì)被標(biāo)記為expunged,程序不會(huì)將被標(biāo)記為expunged的 key 同步過(guò)去。
等到再一次dirty -> read同步的時(shí)候,read會(huì)被dirty直接覆蓋,這個(gè)時(shí)候被標(biāo)記為expunged的key才真正被刪除了,這就是sync.Map的延遲刪除。
到此這篇關(guān)于Golang sync.Map原理深入分析講解的文章就介紹到這了,更多相關(guān)Golang sync.Map內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go語(yǔ)言中嵌套結(jié)構(gòu)體的實(shí)現(xiàn)
在Go語(yǔ)言中,嵌套結(jié)構(gòu)體可定義為一個(gè)結(jié)構(gòu)體內(nèi)包含另一個(gè)結(jié)構(gòu)體,嵌套可以是值嵌套或指針嵌套,兩者在內(nèi)存分配和修改影響上有顯著區(qū)別,本文就來(lái)詳細(xì)的介紹一下,感興趣的可以了解一下2024-09-09
Golang算法問(wèn)題之整數(shù)拆分實(shí)現(xiàn)方法分析
這篇文章主要介紹了Golang算法問(wèn)題之整數(shù)拆分實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了Go語(yǔ)言數(shù)值運(yùn)算與數(shù)組遍歷相關(guān)操作技巧,需要的朋友可以參考下2017-02-02
Golang觀察者模式優(yōu)化訂單處理系統(tǒng)實(shí)例探究
當(dāng)涉及到訂單處理系統(tǒng)時(shí),觀察者設(shè)計(jì)模式可以用于實(shí)現(xiàn)訂單狀態(tài)的變化和通知,在這篇文章中,我們將介紹如何使用Golang來(lái)實(shí)現(xiàn)觀察者設(shè)計(jì)模式,并提供一個(gè)基于訂單處理系統(tǒng)的代碼示例2024-01-01
go編程中g(shù)o-sql-driver的離奇bug解決記錄分析
這篇文章主要為大家介紹了go編程中g(shù)o-sql-driver的離奇bug解決記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05

