goroutine同步方式 | 青训营笔记

55 阅读2分钟

这是我参与「第五届青训营 」笔记创作活动的第15天。

今天继续复习在青训营中学到的golang基础语法。

在实际开发中,经常有需要起多个goroutine互相协作,它们之间又需要共享一些数据的情况。但是如果不加限制的话,就会出现一些问题。

var a = 0
var wg sync.WaitGroup

func add() {
   a++
   wg.Done()
}

func main() {
   for i := 0; i < 1000; i++ {
      wg.Add(1)
      go add()
   }
   wg.Wait()
   fmt.Println(a)
}

这个程序最终的输出是990,而不是1000。原因在于a++并不是原子操作,它在实际执行中大致分为三步:

  1. 读取a的值到寄存器
  2. 寄存器值加1
  3. 将寄存器中的值写回a

假设只有两个gorouti,一旦第二个goroutine在上一个goroutine写回a之前读了a原始值,那么两个goroutine最后的结果都是将初始值加1写回a,而不是初始值加2,这样就出错了。

一个简单粗暴的办法就是,在第一个goroutine执行之前不让别的goroutine访问a。这可以用锁的方式实现:

var m sync.Mutex

func add() {
   m.Lock()
   a++
   wg.Done()
   m.Unlock()
}

m就是互斥锁,一个goroutine在进入add函数后会尝试获取这把锁,即m.Lock()。若获取失败则会阻塞,直到锁可用为止。获取成功后即可执行a++。执行完之后便可释放这把锁,让这把锁可用,并唤醒等待锁的goroutine使之继续运行。

但是goroutine之间的关系除了以上的互斥,还有同步。比如说我有四台“机器”,第一台产生一个数,第二台将收到的数字+1,第三台将收到的数字平方,第四台输出收到的数字。这样一来,每台机器可以看成一个goroutine,每个goroutine能够运行的前提是上一个goroutine给它提供了输入。这样一来这些goroutine之间就有了同步关系,可以用“传送带”也就是通道来实现。

var wg sync.WaitGroup

func produce(out chan int, c chan bool) {
   for i := 0; i < 5; i++ {
      out <- i
   }
   c <- true
   wg.Done()
}

func output(in chan int, c chan bool) {
   for {
      select {
      case val := <-in:
         fmt.Println(val)
      case <-c:
         wg.Done()
         return
      }
   }
}
func main() {
   data := make(chan int)
   closeSig := make(chan bool)
   wg.Add(2)
   go produce(data, closeSig)
   go output(data, closeSig)
   wg.Wait()
}

produce函数不断往data通道中放入int,放完之后向closeSig通道放入一个布尔值(具体是什么无所谓,也可以放空结构体省空间),表示已经放完了。

output函数中有个select语句,它会依次尝试从in和c中取数据,前面的for死循环会让它不断执行。如果从in中取到数据,就输出;如果从c中取到数据,就结束。由于是按序先尝试从in中取数据,如果从c中取到了数据,说明所有数据都处理完了。

最后的结果是依次输出0到4的值。