一、錯誤案例
package main
import (
"fmt"
"time"
)
var TestMap map[string]string
func init() {
TestMap = make(map[string]string, 1)
}
func main() {
for i := 0; i < 1000; i++ {
go Write("aaa")
go Read("aaa")
go Write("bbb")
go Read("bbb")
}
time.Sleep(5 * time.Second)
}
func Read(key string) {
fmt.Println(TestMap[key])
}
func Write(key string) {
TestMap[key] = key
}
上面代碼執行大概率出現報錯:fatal error: concurrent map writes
二、問題分析
網上關於 golang 編程中 map 併發讀寫相關的資料很多,但總是都說成 併發讀寫 造成上面的錯誤,到底是 併發讀 還是 併發寫 造成的,這個很多資料都沒有說明。
我們把上面的案例分別在循環中註釋 Read 和 Write 函數的調用,分別測試 併發讀 和 併發寫;
循環次數分別測試了 100、1 w、100 w 次,併發讀操作絕對不會報上面的錯,而併發寫基本都會報錯。
因此,這個錯誤主要原因是:map 併發寫。
三、問題原因
爲什麼 map 併發寫會導致這個錯誤? 網絡上的相關文章也大都有說明。
因爲 map 變量爲 指針類型變量,併發寫時,多個協程同時操作一個內存,類似於多線程操作同一個資源會發生競爭關係,共享資源會遭到破壞,因此golang 出於安全的考慮,拋出致命錯誤:fatal error: concurrent map writes。
四、解決方案
網上各路資料解決方案較多,主要思路是通過加鎖保證每個協程同步操作內存。
github 上找到一個 concurrentMap 包,案例代碼修改如下:
package main
import (
"fmt"
cmap "github.com/orcaman/concurrent-map"
"time"
)
var TestMap cmap.ConcurrentMap
func init() {
TestMap = cmap.New()
}
func main() {
for i := 0; i < 100; i++ {
go Write("aaa", "111")
go Read("aaa")
go Write("bbb", "222")
go Read("bbb")
}
time.Sleep(5 * time.Second)
}
func Read(key string) {
if v, ok := TestMap.Get(key); ok {
fmt.Printf("鍵值爲 %s 的值爲:%s", key, v)
} else {
fmt.Printf("鍵值不存在")
}
}
func Write(key string, value string) {
TestMap.Set(key, value)
}
五、思考總結
因爲我是以 PHP 打開的編程世界,PHP 語言只有單線程,且不涉及指針操作,變量類型也是弱變量,以 PHP 編程思維剛開始接觸 Golang 時還比較容易上手,但越往後,語言的特性區別就體現得越來越明顯,思維轉變就越來越大,對我來說是打開了一個新世界。
像本文出現的錯誤案例,也是因爲自己沒有多線程編程的思維基礎,導致對這種問題不敏感,還是花了蠻多時間理解的。希望對和我有相似學習路線的朋友提供到一些幫助。