在上一节中,我们了解到程序启动的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
。这个程序的运作原理是这样的:它首先定义一个名为wg
的WaitGroup
变量,然后通过调用wg
的Add
方法将计数器的值设置成2;在此之后,程序会分别调用printNumbers2
和printLetters2
这两个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!
等待组这一特性不仅简单,而且好用,它对并发编程来说是一种不可或缺的工具。