**原文鏈接:** [Go 語言 map 是併發安全的嗎?](https://mp.weixin.qq.com/s/4mDzMdMbunR_p94Du65QOA) Go 語言中的 map 是一個非常常用的數據結構,它允許我們快速地存儲和檢索鍵值對。然而,在併發場景下使用 map 時,還是有一些問題需 ...
原文鏈接: Go 語言 map 是併發安全的嗎?
Go 語言中的 map 是一個非常常用的數據結構,它允許我們快速地存儲和檢索鍵值對。然而,在併發場景下使用 map 時,還是有一些問題需要註意的。
本文將探討 Go 語言中的 map 是否是併發安全的,並提供三種方案來解決併發問題。
先來回答一下題目的問題,答案就是併發不安全。
看一段代碼示例,當兩個 goroutine 同時對同一個 map 進行寫操作時,會發生什麼?
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()
}
在這個例子中,我們可以看到,兩個 goroutine 將嘗試同時對 map 進行寫入。運行這個程式時,我們將看到一個錯誤:
fatal error: concurrent map writes
也就是說,在併發場景下,這樣操作 map 是不行的。
為什麼是不安全的
因為它沒有內置的鎖機制來保護多個 goroutine 同時對其進行讀寫操作。
當多個 goroutine 同時對同一個 map 進行讀寫操作時,就會出現數據競爭和不一致的結果。
就像上例那樣,當兩個 goroutine 同時嘗試更新同一個鍵值對時,最終的結果可能取決於哪個 goroutine 先完成了更新操作。這種不確定性可能會導致程式出現錯誤或崩潰。
Go 語言團隊沒有將 map 設計成併發安全的,是因為這樣會增加程式的開銷並降低性能。
如果 map 內置了鎖機制,那麼每次訪問 map 時都需要進行加鎖和解鎖操作,這會增加程式的運行時間並降低性能。
此外,並不是所有的程式都需要在併發場景下使用 map,因此將鎖機制內置到 map 中會對那些不需要併發安全的程式造成不必要的開銷。
在實際使用過程中,開發人員可以根據程式的需求來選擇是否需要保證 map 的併發安全性,從而在性能和安全性之間做出權衡。
如何併發安全
接下來介紹三種併發安全的方式:
- 讀寫鎖
- 分片加鎖
- sync.Map
加讀寫鎖
第一種方法是使用讀寫鎖,這是最容易想到的一種方式。在讀操作時加讀鎖,在寫操作時加寫鎖。
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
// 啟動多個goroutine進行寫操作
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()
// 啟動多個goroutine進行讀操作
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()
}
在這個示例中,我們定義了一個 SafeMap
結構體,它包含一個 sync.RWMutex
和一個 map[string]string
。
定義了兩個方法:ReadMap
和 WriteMap
。在 ReadMap
方法中,我們使用讀鎖來保護對 map 的讀取操作。在 WriteMap
方法中,我們使用寫鎖來保護對 map 的寫入操作。
在 main
函數中,我們啟動了多個 goroutine 來進行讀寫操作,這些操作都是安全的。
分片加鎖
上例中通過對整個 map 加鎖來實現需求,但相對來說,鎖會大大降低程式的性能,那如何優化呢?其中一個優化思路就是降低鎖的粒度,不對整個 map 進行加鎖。
這種方法是分片加鎖,將這個 map 分成 n 塊,每個塊之間的讀寫操作都互不幹擾,從而降低衝突的可能性。
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
// 啟動多個goroutine進行寫操作
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()
// 啟動多個goroutine進行讀操作
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()
}
在這個示例中,我們定義了一個 SafeMap
結構體,它包含一個長度為 N
的 map 數組和一個長度為 N
的鎖數組。
定義了兩個方法:ReadMap
和 WriteMap
。在這兩個方法中,我們都使用了一個 hash
函數來計算 key
應該存儲在哪個 map 中。然後再對這個 map 進行讀寫操作。
在 main
函數中,我們啟動了多個 goroutine 來進行讀寫操作,這些操作都是安全的。
有一個開源項目 orcaman/concurrent-map 就是通過這種思想來做的,感興趣的同學可以看看。
sync.Map
最後,在內置的 sync 包中(Go 1.9+)也有一個線程安全的 map,通過將讀寫分離的方式實現了某些特定場景下的性能提升。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
// 啟動多個goroutine進行寫操作
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()
// 啟動多個goroutine進行讀操作
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()
}
有了官方的支持,代碼瞬間少了很多,使用起來方便多了。
在這個示例中,我們使用了內置的 sync.Map
類型來存儲鍵值對,使用 Store
方法來存儲鍵值對,使用 Load
方法來獲取鍵值對。
在 main
函數中,我們啟動了多個 goroutine 來進行讀寫操作,這些操作都是安全的。
總結
Go 語言中的 map 本身並不是併發安全的。
在多個 goroutine 同時訪問同一個 map 時,可能會出現併發不安全的現象。這是因為 Go 語言中的 map 並沒有內置鎖來保護對map的訪問。
儘管如此,我們仍然可以使用一些方法來實現 map 的併發安全。
一種方法是使用讀寫鎖,在讀操作時加讀鎖,在寫操作時加寫鎖。
另一種方法是分片加鎖,將這個 map 分成 n 塊,每個塊之間的讀寫操作都互不幹擾,從而降低衝突的可能性。
此外,在內置的 sync 包中(Go 1.9+)也有一個線程安全的 map,它通過將讀寫分離的方式實現了某些特定場景下的性能提升。
以上就是本文的全部內容,如果覺得還不錯的話歡迎點贊,轉發和關註,感謝支持。
參考文章:
推薦閱讀:
- Go 語言切片是如何擴容的?
- Go 語言數組和切片的區別
- Go 語言 new 和 make 關鍵字的區別
- 為什麼 Go 不支持 []T 轉換為 []interface
- 為什麼 Go 語言 struct 要使用 tags