优雅地结束goroutines

本节内容将介绍如何使用go标准库中的sync包来解决上一节提到的Goroutine中的任务还未执行完成,main()函数就提前结束的问题。 本节的代码文件为syncGo.go,我们基于上一节的create.go来扩展syncGo.go。

syncGo.go的第一部分代码如下:

  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "sync"
  6. )

如上所示,我们不再需要time包,我们将使用sync包中的功能来等待所有的Goroutine执行完成。

在第10章“并发 - 高级主题”中,我们将会学习两种方式来对Goroutine进行超时处理。 syncGo.go的第二部分代码如下:

  1. func main() {
  2. n := flag.Int("n", 20, "Number of goroutines")
  3. flag.Parse()
  4. count := *n
  5. fmt.Printf("Going to create %d goroutines.\n", count)
  6. var waitGroup sync.WaitGroup

在上面的代码中,我们定义了sync.WaitGroup类型的变量,查看sync包的源码我们可以发现,waitgroup.go文件位于sync目录中,sync.WaitGroup的定义只不过是一个包含三个字段的结构体:

  1. type WaitGroup struct {
  2. noCopy noCopy
  3. state1 [12]byte
  4. sema uint32
  5. }

syncGo.go的输出将显示有关sync.WaitGroup变量工作方式的更多信息。

syncGo.go的第三部分代码如下:

  1. fmt.Printf("%#v\n", waitGroup)
  2. for i := 0; i < count; i++ {
  3. waitGroup.Add(1)
  4. go func(x int) {
  5. defer waitGroup.Done()
  6. fmt.Printf("%d ", x)
  7. }(i)
  8. }

在这里,您可以使用for循环创建所需数量的Goroutine。(当然,也可以写多个顺序的Go语句。)

每次调用sync.Add()都会增加sync.WaitGroup变量中的计数器。需要注意的是,在go语句之前调用sync.Add(1)非常重要,以防止出现任何竞争条件。当每个Goroutine完成其工作时,将执行sync.Done()函数,以减少相同的计数器。

syncGo.go的最后一部分代码如下:

  1. fmt.Printf("%#v\n", waitGroup)
  2. waitGroup.Wait()
  3. fmt.Println("\nExiting...")
  4. }

sync.Wait()调用将阻塞,直到sync.WaitGroup变量中的计数器为零,从而保证所有Goroutine能执行完成。

syncGo.go的输出如下:

  1. $ go run syncGo.go
  2. Going to create 20 goroutines.
  3. sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0}
  4. sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0}
  5. 19 7 8 9 10 11 12 13 14 15 16 17 0 1 2 5 18 4 6 3
  6. Exiting...
  7. $ go run syncGo.go -n 30
  8. Going to create 30 goroutines.
  9. sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0}
  10. 1 0 4 5 17 7 8 9 10 11 12 13 2 sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 0x17, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0}
  11. 29 15 6 27 24 25 16 22 14 23 18 26 3 19 20 28 21
  12. Exiting...
  13. $ go run syncGo.go -n 30
  14. Going to create 30 goroutines.
  15. sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0}
  16. sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 0x1e, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0}
  17. 29 1 7 8 2 9 10 11 12 4 13 15 0 6 5 22 25 23 16 28 26 20 19 24 21 14 3 17 18 27
  18. Exiting...

syncGo.go的输出因执行情况而异。另外,当Goroutines的数量为30时,一些Goroutine可能会在第二个fmt.Printf(“%#v \ n”,waitGroup)语句之前完成它们的工作。最后需要注意sync.WaitGroup中的state1字段是一个保存计数器的元素,该计数器根据sync.Add()和sync.Done()调用而增加和减少。