Go Map并發(fā)沖突預(yù)防與解決
背景
關(guān)于 Go 語言的 Map,有兩個需要注意的特性:
- Map 是并發(fā)讀寫不安全的,這是出于性能的考慮;
- Map 并發(fā)讀寫導(dǎo)致的錯誤,無法使用
recover捕獲。
后者意味著,只有出現(xiàn)并發(fā)讀寫的問題,服務(wù)就會掛掉。
這兩個特性可能大家都知道,可即使有這個共識,我還是見過這個問題導(dǎo)致的事故。
事故的大致情況是,一個人封裝了map的讀寫,沒有使用鎖。另一個人開協(xié)程讀寫 map。而測試環(huán)境請求量小,不一定會導(dǎo)致崩潰,于是,這個問題就留到生產(chǎn)環(huán)境才出現(xiàn)了。
除了靠開發(fā)者自覺和 code review,還能怎么預(yù)防這種情況呢?我覺得在單元測試加入并行測試也很重要。
并行單元測試
單元測試默認不是并發(fā)的,比如下面的單測,是可以通過的:
func TestConcurrent(t *testing.T) {
var m = map[string]int{}
// 寫 map
t.Run("write", func(t *testing.T) {
for i := 0; i < 10000; i++ {
m["a"] = 1
}
})
// 讀 map
t.Run("read", func(t *testing.T) {
for i := 0; i < 10000; i++ {
_ = m["a"]
}
})
}
但是我們的期望是,上面的單測不通過,該如何解決呢?
testing.T 有一個 Parallel 方法,它表示當(dāng)前測試會和其他測試并行運行。 如果參數(shù)有-test.count或-test.cpu,一個測試可能運行多次,同個測試的多個運行實例,不會并行運行。
我們給上面的單測,加上t.Parallel():
func TestConcurrent(t *testing.T) {
var m = map[string]int{}
t.Run("write", func(t *testing.T) {
// 加上并行
t.Parallel()
for i := 0; i < 10000; i++ {
m["a"] = 1
}
})
t.Run("read", func(t *testing.T) {
// 加上并行
t.Parallel()
for i := 0; i < 10000; i++ {
_ = m["a"]
}
})
}
這次執(zhí)行就會報錯:
fatal error: concurrent map read and map write
支持并發(fā)的 Map
讓 Map 支持并發(fā)讀寫并不麻煩,常見的做法有:
- 操作 map 的時候,加上讀寫鎖
sync.RWMutex; - 使用 sync.Map。
sync.RWMutex 大家用得可能比較多。這里簡單給個demo。
sync.RWMutex
我們給上面的單測加上鎖,這次運行就能通過了。
func TestConcurrent(t *testing.T) {
var m = map[string]int{}
// 定義鎖,零值就可以使用
var mu sync.RWMutex
t.Run("write", func(t *testing.T) {
t.Parallel()
for i := 0; i < 10000; i++ {
// 鎖
mu.Lock()
m["a"] = 1
// 解鎖
mu.Unlock()
}
})
t.Run("read", func(t *testing.T) {
t.Parallel()
for i := 0; i < 10000; i++ {
// 鎖
mu.Lock()
_ = m["a"]
// 解鎖
mu.Unlock()
}
})
}
本文的重點介紹一下Go標(biāo)準庫自帶的,支持并發(fā)讀寫的 map:sync.Map。
sync.Map
sync.Map 就是線程安全版的 map[interface{}]interface{},零值可以直接使用,值不能復(fù)制。它主要用于以下場景:
- 當(dāng)同一個 key 的值,寫少讀多的時候;
- 但多個 goroutines 讀寫或修改一系列不同的key的時候。
上面兩種場景中,比起帶Mutex(或RWMutex)的map,sync.Map 會大大減少鎖的競爭。
sync.Map 提供的方法不多,這里列出一些。注意的是,any 是 go 1.18 中 interface{}的別名。
Store,設(shè)置 key-value。
func (m *Map) Store(key, value any)
Load, 根據(jù) key 讀取 value。
func (m *Map) Load(key any) (value any, ok bool)
Delete,刪除某個key。
func (m *Map) Delete(key any)
Range,遍歷所有key, 如果f返回false,會停止遍歷。
func (m *Map) Range(f func(key, value any) bool)
還有 LoadAndDelete(讀后刪除)、LoadOrStore(讀key,不存在時設(shè)置)。
我們給上面的單測,使用sync.Map,測試也可以通過。
func TestConcurrent(t *testing.T) {
// 可以使用零值
var m sync.Map
t.Run("write", func(t *testing.T) {
t.Parallel()
for i := 0; i < 10000; i++ {
// 寫
m.Store("a", 1)
}
})
t.Run("read", func(t *testing.T) {
t.Parallel()
for i := 0; i < 10000; i++ {
// 讀
v, ok := m.Load("a")
if ok {
_ = v.(int)
}
}
})
}
參考
以上就是Go Map并發(fā)沖突預(yù)防與解決的詳細內(nèi)容,更多關(guān)于Go Map并發(fā)沖突的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go基礎(chǔ)教程系列之回調(diào)函數(shù)和閉包詳解
這篇文章主要介紹了Go基礎(chǔ)教程系列之回調(diào)函數(shù)和閉包詳解,需要的朋友可以參考下2022-04-04
go引入自建包名報錯:package?XXX?is?not?in?std解決辦法
這篇文章主要給大家介紹了go引入自建包名報錯:package?XXX?is?not?in?std的解決辦法,這是在寫測試引入包名的時候遇到的錯誤提示,文中將解決辦法介紹的非常詳細,需要的朋友可以參考下2023-12-12
golang?cache帶索引超時緩存庫實戰(zhàn)示例
這篇文章主要為大家介紹了golang?cache帶索引超時緩存庫實戰(zhàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09

