实现同步的几种方式

  • 互斥锁
  • 条件变量
  • 原子操作
  • chanel
  • sycn.WaitGroup & sync.Once

Channel 与 sync.WaitGroup & sync.Once 的区别

Channel 实现同步的方法

声明一个通道,使得它的数量与我们手动启动 goroutine 的数量相同,之后在利用这个通道使得主 goroutine 等待其他 goroutine 执行结束

  1. func coordinateWithChan() {
  2. sign := make(chan struct{}, 2) // 实现声明channel,使得它的数量与手动启动goroutine的数量相同
  3. num := int32(0)
  4. fmt.Printf("The number: %d [with chan struct{}]\n", num)
  5. max := int32(10)
  6. go addNum(&num, 1, max, func(){
  7. sign <- struct{}{}
  8. })
  9. go addNum(&num, 2, max, func() {
  10. sign <- struct{}{}
  11. })
  12. <- sign
  13. <- sign
  14. }

sync.WaitGroup(结构体类型) 的实现同步方法

sync 包的 WaitGroup 比 channel 更适合一对多的 goroutine 的协作流程
WaitGroup 拥有三个指针方法:

  • Add (WaitGroup内有一个计数器,默认值是0,Add方法可以添加计算器的值)
  • Done (每调用一个该方法,计数器都会自动减1)
  • Wait (若WaitGroup内部计数器不为0,调用Wait方法都会阻塞goroutine的执行)
  1. func coordinateWithGroup() {
  2. var wg sync.WaitGroup
  3. wg.Add(2)
  4. num := int32(0)
  5. fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
  6. max := int32(10)
  7. go addNum(&num, 3, max, wg.Done)
  8. go addNum(&num, 4, max, wg.Done)
  9. wg.Wait()
  10. }

sync.WaitGroup 需要注意的几个问题

WaitGroup类型值中计数器的值不可以小于0

  • 计数器的默认值是0

    WaitGroup值是可以被复用的,但需要保证其计数周期的完整性

  • 一个计数周期的过程:WaitGroup 计数器的值由0变成一个正整数,然后经过一系列过程变成0

  • Wait 方法在某个计数周期被调用就立刻阻塞当前的goroutine,直至计数周期完成
  • 一个Wait方法只能归属于一个计数周期,若Wait方法跨计数周期就会引发panic
  • 不要把增加计数器的add方法和wait方法放到不同的goroutine中,否则会引发panic

sync.Once

Once 类型(结构体类型)的 Do 方法只接受一个参数

  • 这个参数类型必须是func(),即无参数声明、无结果声明的函数
  • 该方法并不是对每个参数函数都执行一次,而是只执行“首次调用时”传入的函数,之后不会执行任何参数函数
  • 如果有多个执行一次的函数,那么就应该为它们分别分配一个 sync.Once 类型的值
  1. func main() {
  2. var counter uint32
  3. var once sync.Once
  4. // 下面两个once.Do只会执行一次
  5. once.Do(func() {
  6. atomic.AddUint32(&counter, 1)
  7. })
  8. fmt.Printf("The counter: %d\n", counter)
  9. once.Do(func() {
  10. atomic.AddUint32(&counter, 2)
  11. })
  12. fmt.Printf("The counter: %d\n", counter)
  13. fmt.Println()
  14. }
  • sync.Once 类型的变量可多次重新复制新的该类型的值
  1. package main
  2. import (
  3. "fmt"
  4. "errors"
  5. "time"
  6. "sync/atomic"
  7. "sync"
  8. )
  9. func main() {
  10. // Demo1
  11. fmt.Println("Demo1")
  12. var counter uint32
  13. var once sync.Once // 第一次声明sync.Once类型变量
  14. once.Do(func() {
  15. atomic.AddUint32(&counter, 1)
  16. })
  17. fmt.Printf("The counter: %d\n", counter)
  18. once.Do(func() {
  19. atomic.AddUint32(&counter, 2)
  20. })
  21. fmt.Printf("The counter: %d\n", counter)
  22. fmt.Println()
  23. // Demo2
  24. fmt.Println("Demo2")
  25. once = sync.Once{} // 再次为sync.Once类型变量重新复制
  26. var wg sync.WaitGroup
  27. wg.Add(3)
  28. go func() {
  29. defer wg.Done()
  30. once.Do(func() {
  31. for i := 0; i < 3; i ++ {
  32. fmt.Printf("Do task. [1-%d]\n", i)
  33. time.Sleep(time.Second)
  34. }
  35. })
  36. fmt.Println("Done. [1]")
  37. }()
  38. go func() {
  39. defer wg.Done()
  40. time.Sleep(time.Microsecond * 500)
  41. once.Do(func() {
  42. fmt.Println("Do task. [2]")
  43. })
  44. fmt.Println("Done. [2]")
  45. }()
  46. go func() {
  47. defer wg.Done()
  48. time.Sleep(time.Microsecond * 500)
  49. once.Do(func() {
  50. fmt.Println("Do task. [3]")
  51. })
  52. fmt.Println("Done. [3]")
  53. }()
  54. wg.Wait()
  55. }

sync.Once类型值的Do方法是怎么保证只执行参数函数一次

  • 这个结构体类型中包含一个 sync.Mutex 类型的字段(建议不要在多个函数相互传递该值)
  • 这个结构体中包含一个uint32类型的done字段,默认是0一旦有Do方法调用完成done字段的值变成1


    为什么done字段的类型是uint32类型?

  1. 原因很简单,因为对它的操作必须是“原子”的
  2. Do方法在一开始就会通过调用atomic.LoadUint32函数来获取该字段的值
  3. 一旦发现该值为1,就会直接返回


  • 多个goroutine同时调用Do方法,还会使用 sync.Mutex 在临界区再次检查done字段是否为1,若为false调用原子操作将done字段改为1
  • Do方法在最后将done改为1的操作是放在defer语句中,因此Do方法内的函数参数无论以什么方式结束都会将done字段改成1

sync.WaitGroup 与 sync.once 的区别

  • WaitGroup 只使用到原子操作
  • sync.once 不但使用原子操作还使用互斥锁
  • 都是开箱即用的,也是并发安全的