在上一节中,我们了解到程序启动的goroutine在程序结束时将会被粗暴地结束,虽然通过Sleep函数来增加时间延迟可以避免这一问题,但这说到底只是一种权宜之计,并没有真正地解决问题。虽然在实际的代码中,程序本身比goroutine更早结束的情况并不多见,但为了避免意外,我们还是需要有一种机制,使程序可以在确保所有goroutine都已经执行完毕的情况下,再执行下一项工作。

为此,Go语言在sync包中提供了一种名为等待组(Wait-Group)的机制,它的运作方式非常简单直接:

  • 声明一个等待组;
  • 使用Add方法为等待组的计数器设置值;
  • 当一个goroutine完成它的工作时,使用Done方法对等待组的计数器执行减一操作;
  • 调用Wait方法,该方法将一直阻塞,直到等待组计数器的值变为0。

代码清单9-6展示了一个使用等待组的例子,在这个例子中,我们复用了之前展示过的printNumbers2函数以及printLetters2函数,并为它们分别加上了1μs的延迟。

代码清单9-6 使用等待组

package main

import (
    "fmt"
    "time"
    "sync"
)

func printNumbers2(wg *sync.WaitGroup) {  
    for i := 0; i < 10; i++ {    
        time.Sleep(1 * time.Microsecond)    
        fmt.Printf("%d ", i)  
    }  
    wg.Done()//对计数器执行减一操作
}

func printLetters2(wg *sync.WaitGroup) {      
    for i := 'A'; i < 'A'+10; i++ {    
        time.Sleep(1 * time.Microsecond)    
        fmt.Printf("%c ", i)  
    }  
    wg.Done()// 对计数器执行减一操作
}

func main() {  
    var wg sync.WaitGroup//声明一个等待组
    wg.Add(2)//为计数器设置值
    go printNumbers2(&wg)  
    go printLetters2(&wg)
    wg.Wait()// 阻塞到计数器的值为0
}

如果我们运行这个程序,那么它将巧妙地打印出0 A 1 B 2C 3 D 4 E 5 F 6 G 7 H 8 I 9 J。这个程序的运作原理是这样的:它首先定义一个名为wgWaitGroup变量,然后通过调用wgAdd方法将计数器的值设置成2;在此之后,程序会分别调用printNumbers2printLetters2这两个goroutine,而这两个goroutine都会在末尾对计数器的值执行减一操作。之后程序会调用等待组的Wait方法,并因此而被阻塞,这一状态将持续到两个goroutine都执行完毕并调用Done方法为止。当程序解除阻塞状态之后,它就会跟平常一样,自然地结束。

如果我们在某个goroutine里面忘记了对计数器执行减一操作,那么等待组将一直阻塞,直到运行时环境发现所有goroutine都已经休眠为止,这时程序将引发一个panic

0 A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I 9 J fatal error: all 
goroutines are asleep - deadlock!

等待组这一特性不仅简单,而且好用,它对并发编程来说是一种不可或缺的工具。

results matching ""

    No results matching ""