詳解Go語言如何解決map并發(fā)安全問題
常說go語言是一門并發(fā)友好的語言,對于并發(fā)操作總會在編譯期完成安全檢查,所以這篇文章我們就來聊聊go語言是如何解決map這個數據結構的線程安全問題。
詳解map中的并發(fā)安全問題
問題復現
我們通過字面量的方式創(chuàng)建一個map集合,然后開啟兩個協程,其中協程1負責寫,協程2負責讀:
func main() { //創(chuàng)建map m := make(map[int]string) //聲明一個長度為2的倒計時門閂 var wg sync.WaitGroup wg.Add(2) //協程1寫 go func() { for true { m[0] = "xiaoming" } wg.Done() }() //協程2讀 go func() { for true { _ = m[0] } wg.Done() }() wg.Wait() fmt.Println("結束") }
在完成編譯后嘗試運行
fatal error: concurrent map read and map write
并發(fā)操作失敗的原因
我們直接假設一個場景,協程并發(fā)場景下當前的map
處于擴容狀態(tài),假設我們的協程1
修改了key-111
對應的元素觸發(fā)漸進式驅逐操作,使得key-111
移動到新桶上,結果協程2
緊隨其后嘗試讀取key-111
對應的元素,結果得到nil
,由此引發(fā)了協程安全問題:
上鎖解決并發(fā)安全問題
和Java
一樣,go語言也有自己的鎖sync.Mutex
,我們在協程進行map操作前后進行上鎖和釋放的鎖的操作,確保單位時間內只有一個協程在操作map
,從而實現協程安全,因為這種鎖是排他鎖,這使得協程的并發(fā)特性得不到發(fā)揮:
var mu sync.Mutex func main() { //創(chuàng)建map m := make(map[int]string) var wg sync.WaitGroup wg.Add(2) //協程1上鎖后寫 go func() { for true { mu.Lock() m[0] = "xiaoming" mu.Unlock() } wg.Done() }() //協程2上鎖后讀 go func() { for true { mu.Lock() _ = m[0] mu.Unlock() } wg.Done() }() wg.Wait() fmt.Println("結束") }
使用自帶的sync.map進行并發(fā)讀寫
好在go語言為我們提供的現成的"輪子"
,即sync.Map
,我們直接通過其內置函數store
和load
即可實現并發(fā)讀寫還能保證協程安全:
func main() { //創(chuàng)建sync.Map var m sync.Map var wg sync.WaitGroup wg.Add(2) //協程1并發(fā)寫 go func() { for true { m.Store(1, "xiaoming") } wg.Done() }() //協程2并發(fā)讀 go func() { for true { m.Load(1) } wg.Done() }() wg.Wait() fmt.Println("結束") }
詳解sync.map并發(fā)操作流程
常規(guī)sync.map并發(fā)讀或寫
sync.map
會有一個read
和dirty
指針,指向不同的key數組
,但是這些key
對應的value
指針都是一樣的,這意味著這個map
不同桶的相同key
共享同一套value
。
進行并發(fā)讀取或者寫的時候,首先拿到一個原子類型的read
指針,通過CAS嘗試修改元素值,如果成功則直接返回,就如下圖所示,我們的協程通過CAS
完成原子指針數值讀取之后,直接操作read
指針所指向的map
元素,通過key
定位到value
完成修改后直接返回。
sync.map修改或追加
接下來再說說另一種情況,假設我們追加一個元素key-24
,通過read
指針進行讀取發(fā)現找不到,這就意味當前元素不存在或者在dirty
指針指向的map
下,所以我們會先上重量級鎖,然后再上一次read
鎖。 分別到read
和dirty
指針上查詢對應key,進行如下三部曲:
- 如果在
read
發(fā)現則修改。 - 如果在
dirty
下發(fā)現則修改。 - 都沒發(fā)現則說明要追加了,則將
amended
設置為true
說明當前map
臟了,嘗試將元素追加到dirty
指針管理的map
下。
這里需要補充一句,通過amended
可知當前map是否處于臟寫狀態(tài),如果這個標志為true,后續(xù)每次讀寫未命中都會對misses
進行自增操作,一旦未命中數達到dirty
數組的長度(大抵是想表達所有未命中的都在dirty
數組上)閾值就會進行一次dirty提升,將dirty的key
提升為read
指針指向的數組,確保提升后續(xù)并發(fā)讀寫的命中率:
sync.map并發(fā)刪除
并發(fā)刪除也和上述并發(fā)讀寫差不多,都是先通過read指針嘗試是否成功,若不成功則鎖主mutex到dirty進行刪除,所以這里就不多贅述了。
sync.map源碼解析
sync.map內存結構
通過上文我們了解了sync.map的基本操作,這里我們再回過頭看看sync.map的數據結構,即重量級鎖mu Mutex,
type Map struct { //重量級鎖 mu Mutex //read指針,指向一個不可變的key數組 read atomic.Pointer[readOnly] //dirty 指針指向可以進行追加操作的key數組 dirty map[any]*entry //當前map讀寫未命中次數 misses int }
sync.Map并發(fā)寫源碼
并發(fā)寫底層本質是調用Swap
進行追加或者修改:
func (m *Map) Store(key, value any) { _, _ = m.Swap(key, value) }
步入swap底層即可看到上文圖解的操作,這里我們給出核心源碼,讀者可自行參閱:
func (m *Map) Swap(key, value any) (previous any, loaded bool) { //上read嘗試修改 read := m.loadReadOnly() if e, ok := read.m[key]; ok { if v, ok := e.trySwap(&value); ok { if v == nil { return nil, false } return *v, true } } //上重量級鎖和read原子指針加載進行修改 m.mu.Lock() read = m.loadReadOnly() if e, ok := read.m[key]; ok { if e.unexpungeLocked() { m.dirty[key] = e } if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else if e, ok := m.dirty[key]; ok { //如果在dirty數組發(fā)現則上swap鎖進行修改 if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else {//上述情況都不符合則將amended 標記為true后進行追加 if !read.amended { m.dirtyLocked() m.read.Store(&readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } //解鎖返回 m.mu.Unlock() return previous, loaded }
sync.Map讀取
對應的讀取源碼即加載read原子變量后嘗試到read指針下讀取,若讀取不到則增加未命中數到dirty指針下讀取:
func (m *Map) Load(key any) (value any, ok bool) { //加載讀原子變量 read := m.loadReadOnly() //嘗試在read指針下讀取 e, ok := read.m[key] //沒讀取到上mutex鎖到dirty下讀取,若發(fā)現則更新未命中數后返回結果 if !ok && read.amended { m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] //更新未命中數 m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load() }
sync.Map刪除
刪除步驟也和前面幾種操作差不多,這里就不多贅述了,讀者可參考筆者核心注釋了解流程:
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) { //上讀鎖定位元素 read := m.loadReadOnly() e, ok := read.m[key] //為命中則上重量級鎖到read和dirty下再次查找,找到了則刪除,若是在dirty下找到還需要額外更新一下未命中數 if !ok && read.amended { m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] delete(m.dirty, key) //自增一次未命中數 m.missLocked() } m.mu.Unlock() } if ok { return e.delete() } return nil, false } // Delete deletes the value for a key. func (m *Map) Delete(key any) { m.LoadAndDelete(key) }
以上就是詳解Go語言如何解決map并發(fā)安全問題的詳細內容,更多關于Go解決map并發(fā)安全的資料請關注腳本之家其它相關文章!
相關文章
Golang 處理浮點數遇到的精度問題(使用decimal)
本文主要介紹了Golang 處理浮點數遇到的精度問題,不使用decimal會出大問題,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02