并發(fā)訪問 slice 如何做到優(yōu)雅和安全?
拋出問題
由于 slice/map 是引用類型,golang 函數(shù)是傳值調(diào)用,所用參數(shù)副本依然是原來的 slice/map, 并發(fā)訪問同一個(gè)資源會(huì)導(dǎo)致競態(tài)條件。
看下面這段代碼:
package main
import (
"fmt"
"sync"
)
func main() {
var (
slc = []int{}
n = 10000
wg sync.WaitGroup
)
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
slc = append(slc, i)
wg.Done()
}()
}
wg.Wait()
fmt.Println("len:", len(slc))
fmt.Println("done")
}
// Output:
len: 8586
done
真實(shí)的輸出并沒有達(dá)到我們的預(yù)期,len(slice) < n。問題出在哪?我們都知道slice是對數(shù)組一個(gè)連續(xù)片段的引用,當(dāng) slice 長度增加的時(shí)候,可能底層的數(shù)組會(huì)被換掉。當(dāng)在換底層數(shù)組之前,切片同時(shí)被多個(gè) goroutine 拿到,并執(zhí)行 append 操作。那么很多 goroutine 的 append 結(jié)果會(huì)被覆蓋,導(dǎo)致 n 個(gè) gouroutine append 后,長度小于n。
那么如何解決這個(gè)問題呢?
map 在 go 1.9 以后官方就給出了 sync.map 的解決方案,但是如果要并發(fā)訪問 slice 就要自己好好設(shè)計(jì)一下了。下面提供兩種方式,幫助你解決這個(gè)問題。
方案 1: 加鎖 ?
func main() {
slc := make([]int, 0, 1000)
var wg sync.WaitGroup
var lock sync.Mutex
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(a int) {
defer wg.Done()
// 加?
lock.Lock()
defer lock.Unlock()
slc = append(slc, a)
}(i)
wg.Wait()
}
fmt.Println(len(slc))
}
優(yōu)點(diǎn)是比較簡單,適合對性能要求不高的場景。
方案 2:使用 channel 串行化操作
type ServiceData struct {
ch chan int // 用來 同步的channel
data []int // 存儲(chǔ)數(shù)據(jù)的slice
}
func (s *ServiceData) Schedule() {
// 從 channel 接收數(shù)據(jù)
for i := range s.ch {
s.data = append(s.data, i)
}
}
func (s *ServiceData) Close() {
// 最后關(guān)閉 channel
close(s.ch)
}
func (s *ServiceData) AddData(v int) {
s.ch <- v // 發(fā)送數(shù)據(jù)到 channel
}
func NewScheduleJob(size int, done func()) *ServiceData {
s := &ServiceData{
ch: make(chan int, size),
data: make([]int, 0),
}
go func() {
// 并發(fā)地 append 數(shù)據(jù)到 slice
s.Schedule()
done()
}()
return s
}
func main() {
var (
wg sync.WaitGroup
n = 1000
)
c := make(chan struct{})
// new 了這個(gè) job 后,該 job 就開始準(zhǔn)備從 channel 接收數(shù)據(jù)了
s := NewScheduleJob(n, func() { c <- struct{}{} })
wg.Add(n)
for i := 0; i < n; i++ {
go func(v int) {
defer wg.Done()
s.AddData(v)
}(i)
}
wg.Wait()
s.Close()
<-c
fmt.Println(len(s.data))
}
實(shí)現(xiàn)相對復(fù)雜,優(yōu)點(diǎn)是性能很好,利用了channel的優(yōu)勢
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關(guān)注
