在使用多个 goroutine 打印内容时,经常因为使用 chan 不恰当而 导致主线程未等待其它 goroutine 全部执行完毕而匆匆推出,造成打印内容不全的问题,这里对其中一种情况进行讲解。
首先是新手易犯错的代码(测试环境 Windows 7 x64,Go 1.1.1,下同):
package main
import (
"fmt"
"runtime"
)
// 从 1 至 1 亿循环叠加,并打印结果。
func print(c chan bool, n int) {
x := 0
for i := 1; i <= 100000000; i++ {
x += i
}
fmt.Println(n, x)
if n == 9 {
c <- true
}
}
func main() {
// 使用多核运行程序
runtime.GOMAXPROCS(runtime.NumCPU())
c := make(chan bool)
for i := 0; i < 10; i++ {
go print(c, i)
}
<-c
fmt.Println("DONE.")
}
这段代码从逻辑上看合乎情理,但是是一种非常 投机取巧 的做法,即根据第 10 个 goroutine 的执行情况来 草率地 认为前面的 9 个 goroutine 都已经执行完毕。如果你将 `runtime.GOMAXPROCS(runtime.NumCPU())` 这句注释掉,使用单核运行程序,则将得到你所预期的效果;但如果使用多核的情况下,这种做法就是 错误的。goroutine 是相互独立的,且在执行过程中可能由于各种原因导致其中几个 goroutine 让出时间片给 CPU 去执行其它 goroutine。所以,我们 不能够依靠 第 10 个 goroutine 的执行结果来判断程序的运行情况。
package main
import (
"fmt"
"runtime"
)
// 从 1 至 1 亿循环叠加,并打印结果。
func print(c chan bool, n int) {
x := 0
for i := 1; i <= 100000000; i++ {
x += i
}
fmt.Println(n, x)
c <- true
}
func main() {
// 使用多核运行程序
runtime.GOMAXPROCS(runtime.NumCPU())
c := make(chan bool, 10)
for i := 0; i < 10; i++ {
go print(c, i)
}
for i := 0; i < 10; i++ {
<-c
}
fmt.Println("DONE.")
}
通过创建一个最大可容纳 10 个对象的 chan 来确保一种 极端情况,即 10 个 goroutine 同时完成打印并向 chan 写入内容。然后,我们通过在 main 函数中进行 10 次读取操作,来确保 chan 中的对象确实被存进去了 10 个,从而保证所有的 goroutine 都执行完毕,然后退出程序。
解决方案二:使用 sync 包的 WaitGroup
package main
import (
"fmt"
"runtime"
"sync"
)
// 从 1 至 1 亿循环叠加,并打印结果。
func print(wg *sync.WaitGroup, n int) {
x := 0
for i := 1; i <= 100000000; i++ {
x += i
}
fmt.Println(n, x)
// 标识一次任务完成
wg.Done()
}
func main() {
// 使用多核运行程序
runtime.GOMAXPROCS(runtime.NumCPU())
// 创建等待组
wg := sync.WaitGroup{}
// 设置需要等待的对象个数
wg.Add(10)
for i := 0; i < 10; i++ {
go print(&wg, i)
}
// 等待所有任务完成
wg.Wait()
fmt.Println("DONE.")
}