背景

有时候在<font style="color:rgb(68, 68, 68);">Go</font>代码中可能会存在多个<font style="color:rgb(68, 68, 68);background-color:rgb(248, 248, 248);">goroutine</font>同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。 举个例子:
  1. var x int64
  2. var wg sync.WaitGroup
  3. func add() {
  4. for i := 0; i < 5000; i++ {
  5. x = x + 1
  6. }
  7. wg.Done()
  8. }
  9. func main() {
  10. wg.Add(2)
  11. go add()
  12. go add()
  13. wg.Wait()
  14. fmt.Println(x)
  15. }
上面的代码中我们开启了两个<font style="color:rgb(68, 68, 68);background-color:rgb(248, 248, 248);">goroutine</font>去累加变量 <font style="color:rgb(68, 68, 68);">x</font> 的值,这两个<font style="color:rgb(68, 68, 68);background-color:rgb(248, 248, 248);">goroutine</font>在访问和修改 <font style="color:rgb(68, 68, 68);background-color:rgb(248, 248, 248);">x</font> 变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

我们利用sync.WaitGroup将上面的代码优化一下

  1. var wg sync.WaitGroup
  2. func hello() {
  3. defer wg.Done()
  4. fmt.Println("Hello Goroutine!")
  5. }
  6. func main() {
  7. wg.Add(1)
  8. go hello() // 启动另外一个goroutine去执行hello函数
  9. fmt.Println("main goroutine done!")
  10. wg.Wait()
  11. }

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。