網站首頁 編程語言 正文
GO語言內置的map
go語言內置一個map數據結構,使用起來非常方便,但是它僅支持并發的讀,不支持并發的寫,比如下面的代碼:
在main函數中開啟兩個協程同時對m進行并發讀和并發寫,程序運行之后會報錯:
package main func main() { m := make(map[int]int) go func() { for { _ = m[1] } }() go func() { for { m[2] = 2 } }() select {} }
改進
既然不可以并發的寫,我們可以給map加一個讀寫鎖,這樣就不會有并發寫沖突的問題了:
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 {} }
這種方式的實現非常簡潔,但也存在一些問題,比如在map的數據非常大的情況下,一把鎖會導致大并發的客戶端共爭一把鎖。
sync.Map
sync.Map是官方在sync包中提供的一種并發map,使用起來非常簡單,和普通map相比,只有遍歷的方式有區別:
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. 讀取或寫入 m.LoadOrStore("peach", 3) }
sync.Map是通過 read 和 dirty 兩個字段將讀寫分離,讀的數據存在只讀字段 read 上,將最新寫入的數據則存在 dirty 字段上。
讀取時會先查詢 read,不存在再查詢 dirty,寫入時則只寫入 dirty。
讀取 read 并不需要加鎖,而讀或寫 dirty 都需要加鎖,另外有 misses 字段來統計 read 被穿透的次數(被穿透指需要讀 dirty 的情況),超過一定次數則將 dirty 數據同步到 read 上,對于刪除數據則直接通過標記來延遲刪除。
在map + 鎖的基礎上,它有著幾個優化點:
- 空間換時間。 通過冗余的兩個數據結構(read、dirty),實現加鎖對性能的影響。
- 使用只讀數據(read),避免讀寫沖突。
- 動態調整,miss次數多了之后,將dirty數據提升為read。
- double-checking。
- 延遲刪除。 刪除一個鍵值只是打標記,只有在提升dirty的時候才清理刪除的數據。
- 優先從read讀取、更新、刪除,因為對read的讀取不需要鎖。
sync.Map原理分析
sync.Map的結構
sync.Map的實現在src/sync/map.go
中,首先來看Map結構體:
type Map struct { // 當涉及到臟數據(dirty)操作時候,需要使用這個鎖 mu Mutex // read是一個只讀數據結構,包含一個map結構, // 讀不需要加鎖,只需要通過 atomic 加載最新的指正即可 read atomic.Value // readOnly // dirty 包含部分map的鍵值對,如果操作需要mutex獲取鎖 // 最后dirty中的元素會被全部提升到read里的map去 dirty map[interface{}]*entry // misses是一個計數器,用于記錄read中沒有的數據而在dirty中有的數據的數量。 // 也就是說如果read不包含這個數據,會從dirty中讀取,并misses+1 // 當misses的數量等于dirty的長度,就會將dirty中的數據遷移到read中 misses int }
上述結構體中的read字段實際上是一個包含map的結構體,該結構體中的map是一個read map,對該map的訪問不需要加鎖,但是增加的元素不會被添加到這個map中,元素會被先增加到dirty中,后續才會被遷移到read只讀map中。
readOnly結構體中還有一個amended字段,該字段是一個標志位,用來表示read map中的數據是否完整。假設當前要查找一個key,會先去read map中找,如果沒有找到,會判斷amended是否為true,如果為true,說明read map的數據不完整,需要去dirty map中找。
// readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { // m包含所有只讀數據,不會進行任何的數據增加和刪除操作 // 但是可以修改entry的指針因為這個不會導致map的元素移動 m map[interface{}]*entry // 標志位,如果為true則表明當前read只讀map的數據不完整,dirty map中包含部分數據 amended bool // true if the dirty map contains some key not in m. }
entry
readOnly.m
和Map.dirty
存儲的值類型是*entry,它包含一個指針p, 指向用戶存儲的value值,結構如下:
type entry struct { p unsafe.Pointer // *interface{} }
其中p對應著三種值:
-
p == nil
: 鍵值已經被刪除,且m.dirty == nil
,這個時候dirty在等待read的同步數據。 -
p == expunged
: 鍵值已經被刪除,但m.dirty!=nil
且 m.dirty 不存在該鍵值(dirty已經得到了read的數據同步,原來為nil的值已經被標記為了expunged沒有被同步過來)。 - 除以上情況,則鍵值對存在,存在于 m.read.m 中,如果 m.dirty!=nil 則也存在于 m.dirty
下面是sync.Map的結構示意圖:
查找
查找元素會調用Load函數,該函數的執行流程:
- 首先去read map中找值,不用加鎖,找到了直接返回結果。
- 如果沒有找到就判斷
read.amended
字段是否為true,true說明dirty中有新數據,應該去dirty中查找,開始加鎖。 - 加完鎖以后又去read map中查找,因為在加鎖的過程中,m.dirty可能被提升為m.read。
- 如果二次檢查沒有找到key,就去
m.dirty
中尋找,然后將misses計數加一。
// 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中查找,這時不需要加鎖 read, _ := m.read.Load().(readOnly) e, ok := read.m[key] // 如果沒有找到,并且read.amended為true,說明dirty中有新數據,從dirty中查找,開始加鎖了 if !ok && read.amended { m.mu.Lock() // 加鎖 // 又在 readonly 中檢查一遍,因為在加鎖的時候 dirty 的數據可能已經遷移到了read中 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // read 還沒有找到,并且dirty中有數據 if !ok && read.amended { e, ok = m.dirty[key] //從 dirty 中查找數據 // 不管m.dirty中存不存在,都將misses + 1 // missLocked() 中滿足條件后就會把m.dirty中數據遷移到m.read中 m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load() }
misses計數
misses計數是有上限的,如果misses次數達到m.dirty
的長度,就開始遷移數據,程序會直接將m.dirty提升為m.read,然后將m.dirty置為nil,等到下次插入新數據的時候,程序才會把read map中的值全部復制給dirty map。
// src/sync/map.go func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) {//misses次數小于 dirty的長度,就不遷移數據,直接返回 return } m.read.Store(readOnly{m: m.dirty}) //開始遷移數據 m.dirty = nil //遷移完dirty就賦值為nil m.misses = 0 //遷移完 misses歸0 }
新增和更新
新增或者更新元素會調用Store函數,該函數的前面幾個步驟與Load函數是一樣的:
- 首先去read map中找值,不用加鎖,找到了直接調用
tryStore()
函數更新值即可。 - 如果沒有找到就開始對dirty map加鎖,加完鎖之后再次去read map中找值,如果存在就判斷該key對應的entry有沒有被標記為
unexpunge
,如果沒有被標記,就直接調用storeLocked()
函數更新值即可。 - 如果在read map中進行二次檢查還是沒有找到key,就去dirty map中找,找到了直接調用
storeLocked()
函數更新值。 - 如果dirty map中也沒有這個key,說明是新加入的key,首先要將
read.amended
標記為true,然后將read map中未刪除的值復制到dirty中,最后向dirty map中加入這個值。
// 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() { // 未被標記成刪除,前面講到entry數據結構時,里面的p值有3種。1.nil 2.expunged,這個值含義有點復雜,可以看看前面entry數據結構 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.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中復制未刪除的數據 m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) //將這個entry加入到m.dirty中 } m.mu.Unlock() }
在Store函數中我們用到了兩個用于更新值的函數:tryStore
以及storeLocked
,tryStore
函數是先判斷p有沒有被標記為expunged(軟刪除),如果被標記了就直接返回false,如果沒有被標記,就將p指向的值進行更新然后返回true。
storeLocked
函數是直接將p指向的值進行更新。
// 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中的值復制到dirty map中:
m.dirtyLocked()
函數用于將read map中的值復制到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 { // 判斷值是否被刪除,被標記為expunged的值不會被復制到read map中 if !e.tryExpungeLocked() { m.dirty[k] = e } } } // expunged實際上是一個指向空接口的unsafe指針 var expunged = unsafe.Pointer(new(interface{})) func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) // 如果p為nil,就會被標記為expunged for p == nil { if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged }
下面是對sync.Map進行讀寫操作的示意圖,正常讀寫且read map中有數據,程序只會訪問read map,而不會去加鎖:
刪除
刪除會調用Delete
函數,該函數的步驟如下:
- 首先去read map中找key,找到了就調用
e.delete()
函數刪除。 - 如果在read map中沒有找到值就開始對dirty map加鎖,加鎖完畢之后再次去read map中查找,找到了就調用
e.delete()
函數刪除。 - 如果二次檢查都沒有找到key(說明這個key是被追加之后,還沒有提升到read map中就要被刪除),就去dirty map中刪除這個key。
// src/sync/map.go // Delete deletes the value for a key. func (m *Map) Delete(key interface{}) { // 從 m.read 中開始查找 read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { // m.read中沒有找到,并且可能存在于m.dirty中,加鎖查找 m.mu.Lock() // 加鎖 read, _ = m.read.Load().(readOnly) // 再在m.read中查找一次 e, ok = read.m[key] if !ok && read.amended { //m.read中又沒找到,amended標志位true,說明在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) // 已標記為刪除 if p == nil || p == expunged { return false } // 原子操作,e.p標記為nil,GO的GC會將對象自動刪除 if atomic.CompareAndSwapPointer(&e.p, p, nil) { return true } } }
key究竟什么時候會被刪除
我們可以發現,如果read map中存在待刪除的key時,程序并不會去直接刪除這個key,而是將這個key對應的p指針指向nil。
在下一次read -> dirty
的同步時,指向nil的p指針會被標記為expunged,程序不會將被標記為expunged的 key 同步過去。
等到再一次dirty -> read
同步的時候,read會被dirty直接覆蓋,這個時候被標記為expunged的key才真正被刪除了,這就是sync.Map的延遲刪除。
原文鏈接:https://blog.csdn.net/qq_49723651/article/details/127633394
相關推薦
- 2022-06-26 Git配置.gitignore文件忽略被指定的文件上傳_相關技巧
- 2023-07-02 Python3中zip()函數知識點小結_python
- 2022-02-13 C語言-操作符(詳細)和表達式求值
- 2022-04-20 C#實現變量交換、斐波那契數列、質數、回文方法合集_C#教程
- 2022-04-01 k8s使用docker作為運行時卡死解決辦法
- 2022-09-09 Go語言中DateTime的用法介紹_Golang
- 2022-04-27 Shell獲取路徑操作(dirname?$0?pwd)的實現_linux shell
- 2023-02-02 C#實現SMTP服務發送郵件的示例代碼_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支