GoLand map中的并發問題——為什么會造成并發問題?該怎么解決?
- 問題提出
- 原因解析
- 具體原因
- 競態檢測器
- 如何解決并發問題呢?
- 方法一 : 使用sync.Mutex
- 方法二: 使用sync.Map
- 我們首先了解一下sync.Map的常用方法:
- Store(key, value interface{})
- Load(key interface{}) (value interface{}, ok bool)
- LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
- Delete(key interface{})
- Range(f func(key, value interface{}) bool)
- 修改之前的代碼
- 總結
- 注釋 : 競態檢測器
問題提出
大家在使用map的時候,一定遇到過一個問題,由于map并不是線程安全,所以就會導致并發問題的出現。
下面先給大家演示一下這個問題:
func main() {m := make(map[string]int, 2)m["dd"] = 22go func() {for {m["ff"] = 1}}()go func() {for {_ = m["dd"]}}()time.Sleep(1 * time.Hour)
}
會出現以下報錯:
fatal error: concurrent map read and map write
為什么會拋出這個錯誤呢?
原因解析
具體原因
這個錯誤其實是“故意”設計給map的,在map的底層代碼里寫好的,為了避免map出現并發問題,用來確保數據的正確性的。
if h.flags&hashWriting != 0 {fatal("concurrent map read and map write")}
map這么設計有兩點好處:
- 保證了map的運行性能,不使用鎖機制降低了程序運行的開銷
- 避免了map運行時造成不可預期的錯誤。比如map的漸進式擴容,在沒有并發的情況下,開啟擴容的前提一定是沒有處于擴容狀態,才能讓每一步操作分擔運行成本;如果并發操作,沒有辦法保證在下一次擴容之前完成了前一次的漸進擴容。
競態檢測器
不過大家可能會發現,map源碼中是有一個競態檢測器的代碼
// 如果啟用了競態檢測并且h不為nil,進行競態檢測。if raceenabled && h != nil {callerpc := getcallerpc()pc := abi.FuncPCABIInternal(mapaccess1)racereadpc(unsafe.Pointer(h), callerpc, pc)raceReadObjectPC(t.Key, key, callerpc, pc)}
這個玩意干什么的呢?為啥有了這個東西還是不能避免并發問題呢?
- 這個東西在平時的生產環境是默認關閉的,開啟需要在執行前輸入"-race",像下面這樣(如果運行有問題,見文末注釋 )
go run -race main.go
- 這個東西只能用來檢測并發程序中的競態條件,并不能規避并發問題!
例如上面舉例的并發錯誤代碼,用-race運行結果是這樣的
D:\GoLand 2024.1.1\program\test
go run -race main.go
==================
WARNING: DATA RACE
Write at 0x00c000020060 by goroutine 6:runtime.mapassign_faststr()D:/GoLand 2024.1.1/Go/src/runtime/map_faststr.go:203 +0x0main.main.func1()D:/GoLand 2024.1.1/program/test/main.go:12 +0x44Previous read at 0x00c000020060 by goroutine 7:runtime.mapaccess1_faststr()D:/GoLand 2024.1.1/Go/src/runtime/map_faststr.go:13 +0x0main.main.func2()D:/GoLand 2024.1.1/program/test/main.go:17 +0x44Goroutine 6 (running) created at:main.main()D:/GoLand 2024.1.1/program/test/main.go:10 +0xc5Goroutine 7 (running) created at:main.main()D:/GoLand 2024.1.1/program/test/main.go:15 +0x130
==================
fatal error: concurrent map read and map writegoroutine 6 [running]:
main.main.func2()D:/GoLand 2024.1.1/program/test/main.go:17 +0x45
created by main.main in goroutine 1D:/GoLand 2024.1.1/program/test/main.go:15 +0x131goroutine 1 [sleep]:
time.Sleep(0x34630b8a000)D:/GoLand 2024.1.1/Go/src/runtime/time.go:195 +0x126
main.main()D:/GoLand 2024.1.1/program/test/main.go:20 +0x145goroutine 5 [runnable]:
main.main.func1()D:/GoLand 2024.1.1/program/test/main.go:12 +0x45
created by main.main in goroutine 1D:/GoLand 2024.1.1/program/test/main.go:10 +0xc6
exit status 2
WARNING: DATA RACE 就意味著發生了并發問題,還有并發問題的詳細信息
如何解決并發問題呢?
有兩種常用方法
方法一 : 使用sync.Mutex
我們可以使用互斥鎖(sync.Mutex)來保護map的并發訪問。在寫入或讀取map之前,我們需要獲取鎖,以確保同一時間只有一個goroutine可以訪問map。
type SafeMap struct {mu sync.Mutexm map[string]int
}func NewSafeMap() *SafeMap {return &SafeMap{m: make(map[string]int),}
}func (sm *SafeMap) Set(key string, value int) {sm.mu.Lock()defer sm.mu.Unlock()sm.m[key] = value
}func (sm *SafeMap) Get(key string) (int, bool) {sm.mu.Lock()defer sm.mu.Unlock()val, ok := sm.m[key]return val, ok
}func main() {m := NewSafeMap()m.Set("dd", 22)go func() {for {m.Set("ff", 1)}}()go func() {for {_, _ = m.Get("dd")}}()time.Sleep(1 * time.Hour)
}
方法二: 使用sync.Map
我們首先了解一下sync.Map的常用方法:
用于添加或更新鍵值對。如果鍵已存在,它的值將被新值覆蓋。
var m sync.Map
m.Store("exampleKey", "exampleValue")
用于獲取鍵對應的值。如果鍵存在,返回鍵對應的值和true;如果不存在,返回nil和false。
if value, ok := m.Load("exampleKey"); ok {fmt.Println("Value found:", value)
}
嘗試從映射中加載鍵的值。如果鍵不存在,它將存儲鍵值對到映射中。返回加載到的值(或存儲的值)和一個布爾值,表示值是否被加載。
if actual, loaded := m.LoadOrStore("exampleKey", "newValue"); loaded {fmt.Println("Value loaded:", actual)
} else {fmt.Println("Value stored:", actual)
}
用于刪除映射中的鍵及其對應的值。
m.Delete("exampleKey")
用于迭代映射中的所有鍵值對。它接受一個函數作為參數,該函數會被調用每個鍵值對。如果該函數返回false,迭代將停止。
m.Range(func(key, value interface{}) bool {fmt.Println("Key:", key, "Value:", value)return true // 繼續迭代
})
修改之前的代碼
func main() {var m sync.Mapm.Store("dd", 22)go func() {for {m.Store("ff", 1)}}()go func() {for {_, _ = m.Load("dd")}}()time.Sleep(1 * time.Hour)
}
總結
- 為了保證性能,將map設置成了不可以并發
- 想要并發操作map,可以使用sync.Mutex 或者sync.Map
注釋 : 競態檢測器
- 大家在使用"-race"啟動的時候,可能會遇到下面的問題:
go: -race requires cgo; enable cgo by setting CGO_ENABLED=1翻譯:Go: -race要求Go;通過設置CGO_ENABLED=1開啟cgo
解決方法 —— 使用env -w 修改環境變量的值:
go env -w CGO_ENABLED=1
- 之后可能還會出現下面的錯誤
cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in %PATH%
先把gcc安裝一下,配置一下環境變量就可以了~