因爲 slice/map 是引用類型,golang函數是傳值調用,所用參數副本依然是原來的 slice, 併發訪問同一個資源會致使竟態條件。golang
看下面這段代碼:數組
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
複製代碼
真實的輸出並無達到咱們的預期,len(slice) < n。 問題出在哪?咱們都知道slice是對數組一個連續片斷的引用,當slice長度增長的時候,可能底層的數組會被換掉。當出在換底層數組以前,切片同時被多個goroutine拿到,並執行append操做。那麼不少goroutine的append結果會被覆蓋,致使n個gouroutine append後,長度小於n。併發
那麼如何解決這個問題呢? map 在 go 1.9 之後官方就給出了 sync.map 的解決方案,可是若是要併發訪問 slice 就要本身好好設計一下了。下面提供兩種方式,幫助你解決這個問題。app
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))
}
複製代碼
優勢是比較簡單,適合對性能要求不高的場景。函數
type ServiceData struct {
ch chan int // 用來 同步的channel
data []int // 存儲數據的slice
}
func (s *ServiceData) Schedule() {
// 從 channel 接收數據
for i := range s.ch {
s.data = append(s.data, i)
}
}
func (s *ServiceData) Close() {
// 最後關閉 channel
close(s.ch)
}
func (s *ServiceData) AddData(v int) {
s.ch <- v // 發送數據到 channel
}
func NewScheduleJob(size int, done func()) *ServiceData {
s := &ServiceData{
ch: make(chan int, size),
data: make([]int, 0),
}
go func() {
// 併發地 append 數據到 slice
s.Schedule()
done()
}()
return s
}
func main() {
var (
wg sync.WaitGroup
n = 1000
)
c := make(chan struct{})
// new 了這個 job 後,該 job 就開始準備從 channel 接收數據了
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))
}
複製代碼
實現相對複雜,優勢是性能很好,利用了channel的優點性能
以上代碼都有比較詳細的註釋,就不展開講了。ui