Golang中slice和map并發(fā)寫入問題解決
本篇文章為大家分享在Golang中,如何實現(xiàn)對slice和map兩種數(shù)據(jù)類型進(jìn)行并發(fā)寫入。對于入門Golang的開發(fā)者來說,可能無法意識到這個問題,這里也會做一個問題演示。
關(guān)于Golang更多互聯(lián)網(wǎng)大廠面試問題,點擊訪問。
切片類型
同步寫入
在下面的代碼中,我們使用for循環(huán)同步模式對一個切片進(jìn)行追加操作。通過結(jié)果可以得出,是預(yù)期的效果。
func?main()?{
?var?slice?[]string
?for?i?:=?0;?i?9999;?i++?{
??slice?=?append(slice,?"demo")
?}
?fmt.Println("slice?len",?len(slice))
}
//?output
slice?len?9999
多協(xié)程寫入
在很多時候,我們?yōu)榱颂岣卟l(fā)能力,會開啟多協(xié)程模式對切片寫入,如下代碼:
func?main()?{
?var?slice?[]string
?for?i?:=?0;?i?9999;?i++?{
??go?func()?{
???slice?=?append(slice,?"demo")
??}()
?}
?fmt.Println("slice?len",?len(slice))
}
//?output
╰─?go?run?demo1.go
slice?len?7680?//?第一次結(jié)果
slice?len?8168?//?第二次結(jié)果
slice?len?7913?//?第三次結(jié)果
通過上圖可以看出,實際追加的數(shù)據(jù)不是我們預(yù)期的結(jié)果。
原理分析
在同步模式下,是一個阻塞式寫入過程。每循環(huán)一次,往切片中追加一個元素,追完完畢之后在進(jìn)行下一次循環(huán)。因此,不會出現(xiàn)追加的元素不正確情況。如下圖:

多協(xié)程寫入下,是一個并發(fā)式寫入過程。我們無法保證每一次的寫都是有序的,存在第一個協(xié)程向某個 索引位寫入數(shù)據(jù)之后,后執(zhí)行的協(xié)程同樣的往這個索引位寫入數(shù)據(jù),就導(dǎo)致前面的協(xié)程寫入數(shù)據(jù)被后面的協(xié)程給覆蓋掉。如下圖:

協(xié)程20得到的索引位和協(xié)程5得到鎖因為是同一個,則協(xié)程20將協(xié)程5寫入的數(shù)據(jù)變成了20。協(xié)程100與協(xié)程6也是同樣原理。因此上述代碼和預(yù)期結(jié)果是有偏差的。
解決方案
通過上述的原理分析,知道了多協(xié)程寫入存在的問題。該如何解決呢?其實我們可以采用上述的同步模式進(jìn)行寫,保證每一個協(xié)程的寫入是有序的就可以了。要解決該問題,我們可以使用鎖。
每次進(jìn)行循環(huán)時,開啟一把鎖。對切片進(jìn)行寫入數(shù)據(jù)。 對切片寫入之后,釋放鎖。進(jìn)行下次循環(huán)。
示例代碼如下:
func?main()?{
?var?slice?[]string
?mutex?:=?sync.RWMutex{}
?for?i?:=?0;?i?9999;?i++?{
??mutex.Lock()
??go?func()?{
???slice?=?append(slice,?"demo")
???mutex.Unlock()
??}()
?}
?fmt.Println("slice?len",?len(slice))
}
//?output
slice?len?9998
這種方案,其實也不難看出存在問題。1是每次循環(huán)都開啟一把鎖,循環(huán)完釋放鎖,這樣性能低。2是最終的結(jié)果是少一個寫入操作。如果對應(yīng)解決方案的可以留言提供解決方案。
map類型
map并發(fā)式寫入數(shù)據(jù),同樣會出現(xiàn)問題。但不會像切片那種直接被覆蓋,而是直接會拋出異常。
func?main()?{
?mapInfo?:=?make(map[int]string)
?for?i?:=?0;?i?10;?i++?{
??go?func(index?int)?{
???mapInfo[index]?=?"demo"
??}(i)
?}
?fmt.Println(len(mapInfo))
}
拋出如下異常:
fatal?error:?concurrent?map?writes
fatal?error:?concurrent?map?writes
goroutine?6?[running]:
runtime.throw(0x10ca607,?0x15)
?/usr/local/go/src/runtime/panic.go:1117?+0x72?fp=0xc000034f60?sp=0xc000034f30?pc=0x10327d2
runtime.mapassign_fast64(0x10b1ee0,?0xc000054030,?0x0,?0x0)
?/usr/local/go/src/runtime/map_fast64.go:176?+0x325?fp=0xc000034fa0?sp=0xc000034f60?pc=0x1010c25
main.main.func1(0xc000054030,?0x0)
Golang默認(rèn)map是不支持并發(fā)寫入操作。
解決方案
要對map做并發(fā)寫入,則需要使用互斥鎖來實現(xiàn),實現(xiàn)并發(fā)讀、同步寫。在使用官方的sync包,有兩種方案,第一種是sync.RWMutex,第二種是sync.map。
sync.RWMutex包實現(xiàn)
func?main()?{
?mapInfo?:=?make(map[int]string)
?mutex?:=?sync.RWMutex{}
?//?使用for循環(huán)模擬多個請求對map進(jìn)行寫操作。
?for?i?:=?0;?i?10000;?i++?{
??mutex.Lock()
??go?func(index?int,?mapInfo?map[int]string)?{
???mapInfo[index]?=?"demo"
???mutex.Unlock()
??}(i,?mapInfo)
?}
?fmt.Println(len(mapInfo))
?//?正常寫法
?mapInfo?:=?make(map[int]string)
?mutex?:=?sync.RWMutex{}
?mutex.Lock()
?mapInfo[0]?=?"demo"
?mutex.Unlock()
}
上述代碼,也可以使用匿名結(jié)構(gòu)體的方式進(jìn)行編寫。
func?main()?{
?var?counter?=?struct?{
??sync.RWMutex
??mapInfo?map[int]string
?}{mapInfo:?make(map[int]string)}
?
?for?i?:=?0;?i?10000;?i++?{
??counter.Lock()
??go?func(index?int,?mapInfo?map[int]string)?{
???mapInfo[index]?=?"demo"
???counter.Unlock()
??}(i,?counter.mapInfo)
?
?}
?fmt.Println(len(counter.mapInfo))
}
使用sync.RWMutex包實現(xiàn),能解決并發(fā)寫入map問題。當(dāng)寫數(shù)據(jù)很多時,開啟一把鎖會導(dǎo)致其他的協(xié)程處于阻塞等待過程中,會導(dǎo)致整體的并發(fā)能力降低。
sync.map包實現(xiàn)
官方在新版本中推薦使用sync.Map來實現(xiàn)并發(fā)寫入操作。sync.Map核心思想是減少鎖,使用空間換取時間。該包實現(xiàn)如下幾個優(yōu)化點:
空間換時間。通過冗余的兩個數(shù)據(jù)結(jié)構(gòu)(read、dirty),實現(xiàn)加鎖對性能的影響。 使用只讀數(shù)據(jù)(read),避免讀寫沖突。 動態(tài)調(diào)整,miss次數(shù)多了之后,將dirty數(shù)據(jù)提升為read。 double-checking。 延遲刪除。刪除一個鍵值只是打標(biāo)記,只有在提升dirty的時候才清理刪除的數(shù)據(jù)。 優(yōu)先從read讀取、更新、刪除,因為對read的讀取不需要鎖。

var?sy?sync.Map
func?main()?{
?sy.Store("name",?"tom")
?sy.Range(func(key,?value?interface{})?bool?{
??fmt.Println(key,?value)
??return?false
?})
}
//?outpt
name?tom
更多關(guān)于sync.Map,可以參考該文章
