詳解Go語言如何解決map并發(fā)安全問題
常說go語言是一門并發(fā)友好的語言,對于并發(fā)操作總會在編譯期完成安全檢查,所以這篇文章我們就來聊聊go語言是如何解決map這個數(shù)據(jù)結(jié)構(gòu)的線程安全問題。

詳解map中的并發(fā)安全問題
問題復(fù)現(xiàn)
我們通過字面量的方式創(chuàng)建一個map集合,然后開啟兩個協(xié)程,其中協(xié)程1負(fù)責(zé)寫,協(xié)程2負(fù)責(zé)讀:
func main() {
//創(chuàng)建map
m := make(map[int]string)
//聲明一個長度為2的倒計時門閂
var wg sync.WaitGroup
wg.Add(2)
//協(xié)程1寫
go func() {
for true {
m[0] = "xiaoming"
}
wg.Done()
}()
//協(xié)程2讀
go func() {
for true {
_ = m[0]
}
wg.Done()
}()
wg.Wait()
fmt.Println("結(jié)束")
}
在完成編譯后嘗試運行
fatal error: concurrent map read and map write
并發(fā)操作失敗的原因
我們直接假設(shè)一個場景,協(xié)程并發(fā)場景下當(dāng)前的map處于擴(kuò)容狀態(tài),假設(shè)我們的協(xié)程1修改了key-111對應(yīng)的元素觸發(fā)漸進(jìn)式驅(qū)逐操作,使得key-111移動到新桶上,結(jié)果協(xié)程2緊隨其后嘗試讀取key-111對應(yīng)的元素,結(jié)果得到nil,由此引發(fā)了協(xié)程安全問題:

上鎖解決并發(fā)安全問題
和Java一樣,go語言也有自己的鎖sync.Mutex,我們在協(xié)程進(jìn)行map操作前后進(jìn)行上鎖和釋放的鎖的操作,確保單位時間內(nèi)只有一個協(xié)程在操作map,從而實現(xiàn)協(xié)程安全,因為這種鎖是排他鎖,這使得協(xié)程的并發(fā)特性得不到發(fā)揮:
var mu sync.Mutex
func main() {
//創(chuàng)建map
m := make(map[int]string)
var wg sync.WaitGroup
wg.Add(2)
//協(xié)程1上鎖后寫
go func() {
for true {
mu.Lock()
m[0] = "xiaoming"
mu.Unlock()
}
wg.Done()
}()
//協(xié)程2上鎖后讀
go func() {
for true {
mu.Lock()
_ = m[0]
mu.Unlock()
}
wg.Done()
}()
wg.Wait()
fmt.Println("結(jié)束")
}
使用自帶的sync.map進(jìn)行并發(fā)讀寫
好在go語言為我們提供的現(xiàn)成的"輪子",即sync.Map,我們直接通過其內(nèi)置函數(shù)store和load即可實現(xiàn)并發(fā)讀寫還能保證協(xié)程安全:
func main() {
//創(chuàng)建sync.Map
var m sync.Map
var wg sync.WaitGroup
wg.Add(2)
//協(xié)程1并發(fā)寫
go func() {
for true {
m.Store(1, "xiaoming")
}
wg.Done()
}()
//協(xié)程2并發(fā)讀
go func() {
for true {
m.Load(1)
}
wg.Done()
}()
wg.Wait()
fmt.Println("結(jié)束")
}
詳解sync.map并發(fā)操作流程
常規(guī)sync.map并發(fā)讀或?qū)?/h3>
sync.map會有一個read和dirty指針,指向不同的key數(shù)組,但是這些key對應(yīng)的value指針都是一樣的,這意味著這個map不同桶的相同key共享同一套value。
進(jìn)行并發(fā)讀取或者寫的時候,首先拿到一個原子類型的read指針,通過CAS嘗試修改元素值,如果成功則直接返回,就如下圖所示,我們的協(xié)程通過CAS完成原子指針數(shù)值讀取之后,直接操作read指針?biāo)赶虻?code>map元素,通過key定位到value完成修改后直接返回。

sync.map修改或追加
接下來再說說另一種情況,假設(shè)我們追加一個元素key-24,通過read指針進(jìn)行讀取發(fā)現(xiàn)找不到,這就意味當(dāng)前元素不存在或者在dirty指針指向的map下,所以我們會先上重量級鎖,然后再上一次read鎖。 分別到read和dirty指針上查詢對應(yīng)key,進(jìn)行如下三部曲:
- 如果在
read發(fā)現(xiàn)則修改。 - 如果在
dirty下發(fā)現(xiàn)則修改。 - 都沒發(fā)現(xiàn)則說明要追加了,則將
amended設(shè)置為true說明當(dāng)前map臟了,嘗試將元素追加到dirty指針管理的map下。

這里需要補充一句,通過amended可知當(dāng)前map是否處于臟寫狀態(tài),如果這個標(biāo)志為true,后續(xù)每次讀寫未命中都會對misses進(jìn)行自增操作,一旦未命中數(shù)達(dá)到dirty數(shù)組的長度(大抵是想表達(dá)所有未命中的都在dirty數(shù)組上)閾值就會進(jìn)行一次dirty提升,將dirty的key提升為read指針指向的數(shù)組,確保提升后續(xù)并發(fā)讀寫的命中率:

sync.map并發(fā)刪除
并發(fā)刪除也和上述并發(fā)讀寫差不多,都是先通過read指針嘗試是否成功,若不成功則鎖主mutex到dirty進(jìn)行刪除,所以這里就不多贅述了。
sync.map源碼解析
sync.map內(nèi)存結(jié)構(gòu)
通過上文我們了解了sync.map的基本操作,這里我們再回過頭看看sync.map的數(shù)據(jù)結(jié)構(gòu),即重量級鎖mu Mutex,
type Map struct {
//重量級鎖
mu Mutex
//read指針,指向一個不可變的key數(shù)組
read atomic.Pointer[readOnly]
//dirty 指針指向可以進(jìn)行追加操作的key數(shù)組
dirty map[any]*entry
//當(dāng)前map讀寫未命中次數(shù)
misses int
}
sync.Map并發(fā)寫源碼
并發(fā)寫底層本質(zhì)是調(diào)用Swap進(jìn)行追加或者修改:
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原子指針加載進(jìn)行修改
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數(shù)組發(fā)現(xiàn)則上swap鎖進(jìn)行修改
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else {//上述情況都不符合則將amended 標(biāo)記為true后進(jìn)行追加
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讀取
對應(yīng)的讀取源碼即加載read原子變量后嘗試到read指針下讀取,若讀取不到則增加未命中數(shù)到dirty指針下讀?。?/p>
func (m *Map) Load(key any) (value any, ok bool) {
//加載讀原子變量
read := m.loadReadOnly()
//嘗試在read指針下讀取
e, ok := read.m[key]
//沒讀取到上mutex鎖到dirty下讀取,若發(fā)現(xiàn)則更新未命中數(shù)后返回結(jié)果
if !ok && read.amended {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
//更新未命中數(shù)
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下找到還需要額外更新一下未命中數(shù)
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)
//自增一次未命中數(shù)
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ā)安全問題的詳細(xì)內(nèi)容,更多關(guān)于Go解決map并發(fā)安全的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang 處理浮點數(shù)遇到的精度問題(使用decimal)
本文主要介紹了Golang 處理浮點數(shù)遇到的精度問題,不使用decimal會出大問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02
Go語言題解LeetCode268丟失的數(shù)字示例詳解
這篇文章主要為大家介紹了Go語言題解LeetCode268丟失的數(shù)字示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
詳解Go如何實現(xiàn)協(xié)程并發(fā)執(zhí)行
線程是通過本地隊列,全局隊列或者偷其它線程的方式來獲取協(xié)程的,目前看來,線程運行完一個協(xié)程后再從隊列中獲取下一個協(xié)程執(zhí)行,還只是順序執(zhí)行協(xié)程的,而多個線程一起這么運行也能達(dá)到并發(fā)的效果,接下來就給給大家詳細(xì)介紹一下Go如何實現(xiàn)協(xié)程并發(fā)執(zhí)行2023-08-08

