併發訪問 slice 如何作到優雅和安全?

拋出問題

因爲 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

方案 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))
}
複製代碼

優勢是比較簡單,適合對性能要求不高的場景。函數

方案 2: 使用 channel 串行化操做

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

相關文章
相關標籤/搜索