Go 語(yǔ)言中的 map 是一個(gè)非常常用的數(shù)據(jù)結(jié)構(gòu),它允許我們快速地存儲(chǔ)和檢索鍵值對(duì)。然而,在并發(fā)場(chǎng)景下使用 map 時(shí),還是有一些問題需要注意的。
本文將探討 Go 語(yǔ)言中的 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)樗鼪]有內(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 語(yǔ)言團(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è)長(zhǎng)度為 N 的 map 數(shù)組和一個(gè)長(zhǎng)度為 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 語(yǔ)言中的 map 本身并不是并發(fā)安全的。
在多個(gè) goroutine 同時(shí)訪問同一個(gè) map 時(shí),可能會(huì)出現(xiàn)并發(fā)不安全的現(xiàn)象。這是因?yàn)?Go 語(yǔ)言中的 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)景下的性能提升。
以上就是本文的全部?jī)?nèi)容,如果覺得還不錯(cuò)的話歡迎點(diǎn)贊,轉(zhuǎn)發(fā)和關(guān)注,感謝支持。
參考文章:
- https://zhuanlan.zhihu.com/p/356739568