并发访问 slice 如何做到优雅和安全?

共 358字,需浏览 1分钟

 ·

2020-08-18 00:16

抛出问题

由于 slice/map 是引用类型,golang 函数是传值调用,所用参数副本依然是原来的 slice/map, 并发访问同一个资源会导致竞态条件。

看下面这段代码:

  1. package main


  2. import (

  3. "fmt"

  4. "sync"

  5. )


  6. func main() {

  7. var (

  8. slc = []int{}

  9. n = 10000

  10. wg sync.WaitGroup

  11. )


  12. wg.Add(n)

  13. for i := 0; i < n; i++ {

  14. go func() {

  15. slc = append(slc, i)

  16. wg.Done()

  17. }()

  18. }

  19. wg.Wait()


  20. fmt.Println("len:", len(slc))

  21. fmt.Println("done")

  22. }


  23. // Output:

  24. len: 8586

  25. done

真实的输出并没有达到我们的预期,len(slice) < n。问题出在哪?我们都知道slice是对数组一个连续片段的引用,当 slice 长度增加的时候,可能底层的数组会被换掉。当在换底层数组之前,切片同时被多个 goroutine 拿到,并执行 append 操作。那么很多 goroutine 的 append 结果会被覆盖,导致 n 个 gouroutine append 后,长度小于n。

那么如何解决这个问题呢?

map 在 go 1.9 以后官方就给出了 sync.map 的解决方案,但是如果要并发访问 slice 就要自己好好设计一下了。下面提供两种方式,帮助你解决这个问题。

方案 1: 加锁 ?

  1. func main() {

  2. slc := make([]int, 0, 1000)

  3. var wg sync.WaitGroup

  4. var lock sync.Mutex


  5. for i := 0; i < 1000; i++ {

  6. wg.Add(1)

  7. go func(a int) {

  8. defer wg.Done()

  9. // 加?

  10. lock.Lock()

  11. defer lock.Unlock()

  12. slc = append(slc, a)

  13. }(i)

  14. wg.Wait()


  15. }


  16. fmt.Println(len(slc))

  17. }

优点是比较简单,适合对性能要求不高的场景。

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

  1. type ServiceData struct {

  2. ch chan int // 用来 同步的channel

  3. data []int // 存储数据的slice

  4. }


  5. func (s *ServiceData) Schedule() {

  6. // 从 channel 接收数据

  7. for i := range s.ch {

  8. s.data = append(s.data, i)

  9. }

  10. }


  11. func (s *ServiceData) Close() {

  12. // 最后关闭 channel

  13. close(s.ch)

  14. }


  15. func (s *ServiceData) AddData(v int) {

  16. s.ch <- v // 发送数据到 channel

  17. }


  18. func NewScheduleJob(size int, done func()) *ServiceData {

  19. s := &ServiceData{

  20. ch: make(chan int, size),

  21. data: make([]int, 0),

  22. }


  23. go func() {

  24. // 并发地 append 数据到 slice

  25. s.Schedule()

  26. done()

  27. }()


  28. return s

  29. }


  30. func main() {

  31. var (

  32. wg sync.WaitGroup

  33. n = 1000

  34. )

  35. c := make(chan struct{})


  36. // new 了这个 job 后,该 job 就开始准备从 channel 接收数据了

  37. s := NewScheduleJob(n, func() { c <- struct{}{} })


  38. wg.Add(n)

  39. for i := 0; i < n; i++ {

  40. go func(v int) {

  41. defer wg.Done()

  42. s.AddData(v)


  43. }(i)

  44. }


  45. wg.Wait()

  46. s.Close()

  47. <-c


  48. fmt.Println(len(s.data))

  49. }

实现相对复杂,优点是性能很好,利用了channel的优势




推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注


浏览 11
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报