深入探討Go語言中的map是否是并發(fā)安全以及解決方法
Go 語言中的 map 是一個(gè)非常常用的數(shù)據(jù)結(jié)構(gòu),它允許我們快速地存儲(chǔ)和檢索鍵值對(duì)。然而,在并發(fā)場(chǎng)景下使用 map 時(shí),還是有一些問題需要注意的。
本文將探討 Go 語言中的 map 是否是并發(fā)安全的,并提供三種方案來解決并發(fā)問題。
先來回答一下題目的問題,答案就是并發(fā)不安全。
看一段代碼示例,當(dāng)兩個(gè) goroutine 同時(shí)對(duì)同一個(gè) map 進(jìn)行寫操作時(shí),會(huì)發(fā)生什么?
package?main import?"sync" func?main()?{ ????m?:=?make(map[string]int) ????m["foo"]?=?1 ????var?wg?sync.WaitGroup ????wg.Add(2) ????go?func()?{ ????????for?i?:=?0;?i?<?1000;?i++?{ ????????????m["foo"]++ ????????} ????????wg.Done() ????}() ????go?func()?{ ????????for?i?:=?0;?i?<?1000;?i++?{ ????????????m["foo"]++ ????????} ????????wg.Done() ????}() ????wg.Wait() }
在這個(gè)例子中,我們可以看到,兩個(gè) goroutine 將嘗試同時(shí)對(duì) map 進(jìn)行寫入。運(yùn)行這個(gè)程序時(shí),我們將看到一個(gè)錯(cuò)誤:
fatal error: concurrent map writes
也就是說,在并發(fā)場(chǎng)景下,這樣操作 map 是不行的。
為什么是不安全的
因?yàn)樗?strong>沒有內(nèi)置的鎖機(jī)制來保護(hù)多個(gè) goroutine 同時(shí)對(duì)其進(jìn)行讀寫操作。
當(dāng)多個(gè) goroutine 同時(shí)對(duì)同一個(gè) map 進(jìn)行讀寫操作時(shí),就會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)和不一致的結(jié)果。
就像上例那樣,當(dāng)兩個(gè) goroutine 同時(shí)嘗試更新同一個(gè)鍵值對(duì)時(shí),最終的結(jié)果可能取決于哪個(gè) goroutine 先完成了更新操作。這種不確定性可能會(huì)導(dǎo)致程序出現(xiàn)錯(cuò)誤或崩潰。
Go 語言團(tuán)隊(duì)沒有將 map 設(shè)計(jì)成并發(fā)安全的,是因?yàn)檫@樣會(huì)增加程序的開銷并降低性能。
如果 map 內(nèi)置了鎖機(jī)制,那么每次訪問 map 時(shí)都需要進(jìn)行加鎖和解鎖操作,這會(huì)增加程序的運(yùn)行時(shí)間并降低性能。
此外,并不是所有的程序都需要在并發(fā)場(chǎng)景下使用 map,因此將鎖機(jī)制內(nèi)置到 map 中會(huì)對(duì)那些不需要并發(fā)安全的程序造成不必要的開銷。
在實(shí)際使用過程中,開發(fā)人員可以根據(jù)程序的需求來選擇是否需要保證 map 的并發(fā)安全性,從而在性能和安全性之間做出權(quán)衡。
如何并發(fā)安全
接下來介紹三種并發(fā)安全的方式:
- 讀寫鎖
- 分片加鎖
- sync.Map
加讀寫鎖
第一種方法是使用讀寫鎖,這是最容易想到的一種方式。在讀操作時(shí)加讀鎖,在寫操作時(shí)加寫鎖。
package?main import?( ????"fmt" ????"sync" ) type?SafeMap?struct?{ ????sync.RWMutex ????Map?map[string]string } func?NewSafeMap()?*SafeMap?{ ????sm?:=?new(SafeMap) ????sm.Map?=?make(map[string]string) ????return?sm } func?(sm?*SafeMap)?ReadMap(key?string)?string?{ ????sm.RLock() ????value?:=?sm.Map[key] ????sm.RUnlock() ????return?value } func?(sm?*SafeMap)?WriteMap(key?string,?value?string)?{ ????sm.Lock() ????sm.Map[key]?=?value ????sm.Unlock() } func?main()?{ ????safeMap?:=?NewSafeMap() ????var?wg?sync.WaitGroup ????//?啟動(dòng)多個(gè)goroutine進(jìn)行寫操作 ????for?i?:=?0;?i?<?10;?i++?{ ????????wg.Add(1) ????????go?func(i?int)?{ ????????????defer?wg.Done() ????????????safeMap.WriteMap(fmt.Sprintf("name%d",?i),?fmt.Sprintf("John%d",?i)) ????????}(i) ????} ????wg.Wait() ????//?啟動(dòng)多個(gè)goroutine進(jìn)行讀操作 ????for?i?:=?0;?i?<?10;?i++?{ ????????wg.Add(1) ????????go?func(i?int)?{ ????????????defer?wg.Done() ????????????fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d",?i))) ????????}(i) ????} ????wg.Wait() }
在這個(gè)示例中,我們定義了一個(gè) SafeMap
結(jié)構(gòu)體,它包含一個(gè) sync.RWMutex
和一個(gè) map[string]string
。
定義了兩個(gè)方法:ReadMap
和 WriteMap
。在 ReadMap
方法中,我們使用讀鎖來保護(hù)對(duì) map 的讀取操作。在 WriteMap
方法中,我們使用寫鎖來保護(hù)對(duì) map 的寫入操作。
在 main
函數(shù)中,我們啟動(dòng)了多個(gè) goroutine 來進(jìn)行讀寫操作,這些操作都是安全的。
分片加鎖
上例中通過對(duì)整個(gè) map 加鎖來實(shí)現(xiàn)需求,但相對(duì)來說,鎖會(huì)大大降低程序的性能,那如何優(yōu)化呢?其中一個(gè)優(yōu)化思路就是降低鎖的粒度,不對(duì)整個(gè) map 進(jìn)行加鎖。
這種方法是分片加鎖,將這個(gè) map 分成 n 塊,每個(gè)塊之間的讀寫操作都互不干擾,從而降低沖突的可能性。
package?main import?( ????"fmt" ????"sync" ) const?N?=?16 type?SafeMap?struct?{ ????maps??[N]map[string]string ????locks?[N]sync.RWMutex } func?NewSafeMap()?*SafeMap?{ ????sm?:=?new(SafeMap) ????for?i?:=?0;?i?<?N;?i++?{ ????????sm.maps[i]?=?make(map[string]string) ????} ????return?sm } func?(sm?*SafeMap)?ReadMap(key?string)?string?{ ????index?:=?hash(key)?%?N ????sm.locks[index].RLock() ????value?:=?sm.maps[index][key] ????sm.locks[index].RUnlock() ????return?value } func?(sm?*SafeMap)?WriteMap(key?string,?value?string)?{ ????index?:=?hash(key)?%?N ????sm.locks[index].Lock() ????sm.maps[index][key]?=?value ????sm.locks[index].Unlock() } func?hash(s?string)?int?{ ????h?:=?0 ????for?i?:=?0;?i?<?len(s);?i++?{ ????????h?=?31*h?+?int(s[i]) ????} ????return?h } func?main()?{ ????safeMap?:=?NewSafeMap() ????var?wg?sync.WaitGroup ????//?啟動(dòng)多個(gè)goroutine進(jìn)行寫操作 ????for?i?:=?0;?i?<?10;?i++?{ ????????wg.Add(1) ????????go?func(i?int)?{ ????????????defer?wg.Done() ????????????safeMap.WriteMap(fmt.Sprintf("name%d",?i),?fmt.Sprintf("John%d",?i)) ????????}(i) ????} ????wg.Wait() ????//?啟動(dòng)多個(gè)goroutine進(jìn)行讀操作 ????for?i?:=?0;?i?<?10;?i++?{ ????????wg.Add(1) ????????go?func(i?int)?{ ????????????defer?wg.Done() ????????????fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d",?i))) ????????}(i) ????} ????wg.Wait() }
在這個(gè)示例中,我們定義了一個(gè) SafeMap
結(jié)構(gòu)體,它包含一個(gè)長度為 N
的 map 數(shù)組和一個(gè)長度為 N
的鎖數(shù)組。
定義了兩個(gè)方法:ReadMap
和 WriteMap
。在這兩個(gè)方法中,我們都使用了一個(gè) hash
函數(shù)來計(jì)算 key
應(yīng)該存儲(chǔ)在哪個(gè) map 中。然后再對(duì)這個(gè) map 進(jìn)行讀寫操作。
在 main
函數(shù)中,我們啟動(dòng)了多個(gè) goroutine 來進(jìn)行讀寫操作,這些操作都是安全的。
有一個(gè)開源項(xiàng)目 orcaman/concurrent-map 就是通過這種思想來做的,感興趣的同學(xué)可以看看。
sync.Map
最后,在內(nèi)置的 sync 包中(Go 1.9+)也有一個(gè)線程安全的 map,通過將讀寫分離的方式實(shí)現(xiàn)了某些特定場(chǎng)景下的性能提升。
package?main import?( ????"fmt" ????"sync" ) func?main()?{ ????var?m?sync.Map ????var?wg?sync.WaitGroup ????//?啟動(dòng)多個(gè)goroutine進(jìn)行寫操作 ????for?i?:=?0;?i?<?10;?i++?{ ????????wg.Add(1) ????????go?func(i?int)?{ ????????????defer?wg.Done() ????????????m.Store(fmt.Sprintf("name%d",?i),?fmt.Sprintf("John%d",?i)) ????????}(i) ????} ????wg.Wait() ????//?啟動(dòng)多個(gè)goroutine進(jìn)行讀操作 ????for?i?:=?0;?i?<?10;?i++?{ ????????wg.Add(1) ????????go?func(i?int)?{ ????????????defer?wg.Done() ????????????v,?_?:=?m.Load(fmt.Sprintf("name%d",?i)) ????????????fmt.Println(v.(string)) ????????}(i) ????} ????wg.Wait() }
有了官方的支持,代碼瞬間少了很多,使用起來方便多了。
在這個(gè)示例中,我們使用了內(nèi)置的 sync.Map
類型來存儲(chǔ)鍵值對(duì),使用 Store
方法來存儲(chǔ)鍵值對(duì),使用 Load
方法來獲取鍵值對(duì)。
在 main
函數(shù)中,我們啟動(dòng)了多個(gè) goroutine 來進(jìn)行讀寫操作,這些操作都是安全的。
總結(jié)
Go 語言中的 map 本身并不是并發(fā)安全的。
在多個(gè) goroutine 同時(shí)訪問同一個(gè) map 時(shí),可能會(huì)出現(xiàn)并發(fā)不安全的現(xiàn)象。這是因?yàn)?Go 語言中的 map 并沒有內(nèi)置鎖來保護(hù)對(duì)map的訪問。
盡管如此,我們?nèi)匀豢梢允褂靡恍┓椒▉韺?shí)現(xiàn) map 的并發(fā)安全。
一種方法是使用讀寫鎖,在讀操作時(shí)加讀鎖,在寫操作時(shí)加寫鎖。
另一種方法是分片加鎖,將這個(gè) map 分成 n 塊,每個(gè)塊之間的讀寫操作都互不干擾,從而降低沖突的可能性。
此外,在內(nèi)置的 sync 包中(Go 1.9+)也有一個(gè)線程安全的 map,它通過將讀寫分離的方式實(shí)現(xiàn)了某些特定場(chǎng)景下的性能提升。
到此這篇關(guān)于深入探討Go語言中的map是否是并發(fā)安全以及解決方法的文章就介紹到這了,更多相關(guān)Go語言map內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文詳解GO如何實(shí)現(xiàn)Redis的AOF持久化
這篇文章主要為大家詳細(xì)介紹了GO如何實(shí)現(xiàn)Redis的AOF持久化的,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解一下2023-03-03在golang中使用Sync.WaitGroup解決等待的問題
這篇文章主要介紹了在golang中使用Sync.WaitGroup解決等待的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04Go標(biāo)準(zhǔn)庫Flag庫和Log庫的使用
本文主要介紹了Go標(biāo)準(zhǔn)庫Flag庫和Log庫的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-05-05golang實(shí)現(xiàn)對(duì)JavaScript代碼混淆
在Go語言中,你可以使用一些工具來混淆JavaScript代碼,一個(gè)常用的工具是Terser,它可以用于壓縮和混淆JavaScript代碼,你可以通過Go語言的`os/exec`包來調(diào)用Terser工具,本文給通過一個(gè)簡單的示例給大家介紹一下,感興趣的朋友可以參考下2024-01-01