Go 的切片支援併發嗎?

語言: CN / TW / HK

前言

哈嘍,今天與大家聊一個比較冷門的高頻面試題,關於切片的, Go 語言中的切片原生支援併發嗎?怎麼樣,心裡有答案了嘛,帶著你的思考我們一起來看一看這個知識點。 本文轉載自公眾號「Golang夢工廠」,推薦關注!

實踐檢驗真理

實踐是檢驗真理的唯一標準,所以當我們遇到一個不確定的問題,直接寫demo來驗證,因為切片的特點,我們可以分多種情況來驗證:

  1. 不指定索引,動態擴容併發向切片新增資料

func concurrentAppendSliceNotForceIndex() {
sl := make([]int, 0)
wg := sync.WaitGroup{}
for index := 0; index < 100; index++{
k := index
wg.Add(1)
go func(num int) {
sl = append(sl, num)
wg.Done()
}(k)
}
wg.Wait()
fmt.Printf("final len(sl)=%d cap(sl)=%d\n", len(sl), cap(sl))
}

通過列印資料發現每次的結果都不一致,先不急出結論,我們在寫其他的demo測試一下;

  1. 指定索引,指定容量併發向切片新增資料

func concurrentAppendSliceForceIndex() {
sl := make([]int, 100)
wg := sync.WaitGroup{}
for index := 0; index < 100; index++{
k := index
wg.Add(1)
go func(num int) {
sl[num] = num
wg.Done()
}(k)
}
wg.Wait()
fmt.Printf("final len(sl)=%d cap(sl)=%d\n", len(sl), cap(sl))
}

通過結果我們可以發現符合我們的預期,長度和容量都是100,所以說slice支援併發嗎?

slice支援併發嗎?

我們都知道切片是對陣列的抽象,其底層就是陣列,在併發下寫資料到相同的索引位會被覆蓋,並且切片也有自動擴容的功能,當切片要進行擴容時,就要替換底層的陣列,在切換底層陣列時,多個 goroutine 是同時執行的,哪個 goroutine 先執行是不確定的,不論哪個 goroutine 先寫入記憶體,肯定就有一次寫入會覆蓋之前的寫入,所以在動態擴容時併發寫入陣列是不安全的;

所以當別人問你 slice 支援併發時,你就可以這樣回答它:

當指定索引使用切片時,切片是支援併發讀寫索引區的資料的,但是索引區的資料在併發時會被覆蓋的;當不指定索引切片時,並且切片動態擴容時,併發場景下擴容會被覆蓋,所以切片是不支援併發的~。

github 上著名的 iris 框架也曾遇到過切片動態擴容導致 webscoket 連線數減少的 bug ,最終採用 sync.map 解決了該問題,感興趣的可以看一下這個 issue :https://github.com/kataras/iris/pull/1023#event-1777396646;

總結

針對上述問題,我們可以多種方法來解決切片併發安全的問題:

  1. 加互斥鎖

  2. 使用 channel 序列化操作
  3. 使用 sync.map 代替切片

切片的問題還是比較容易解決,針對不同的場景可以選擇不同的方案進行優化,你學會了嗎?