在实际使用Go协程实现并行应用时,可能会遇到这样场景:需要阻塞部分代码执行,直到其他协程成功执行之后才继续执行。
示例代码:
package main
import "fmt"
func myFunc() {
fmt.Println("hello my goroutine")
}
func main() {
fmt.Println("Hello World")
go myFunc()
fmt.Println("main goroutine done")}
结果如下:
Hello World
main goroutine done
协程内的信息"hello my goroutine"并没有出现。在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。说白了就是因为main在协程执行之前已经结束,所以协程中的逻辑并未执行。
同步等待组(WaitGroups)就是要解决这类问题,阻塞应用直到同步等待组中的所有协程都成功执行。
WatiGroup是sync包中的一个struct类型,用来收集需要等待执行完成的goroutine。
它有3个方法:
- Add():每次激活想要被等待完成的goroutine之前,先调用Add(),用来设置或添加要等待完成的goroutine数量
- 例如Add(2)或者两次调用Add(1)都会设置等待计数器的值为2,表示要等待2个goroutine完成
- Done():每次需要等待的goroutine在真正完成之前,应该调用该方法来人为表示goroutine完成了,该方法会对等待计数器减1
- Wait():在等待计数器减为0之前,Wait()会一直阻塞当前的goroutine
所以我们代码改一下:
package main
import (
"fmt"
"sync"
)
func myFunc(waitgroup *sync.WaitGroup) {
fmt.Println("hello my goroutine")
waitgroup.Done()
}
func main() {
fmt.Println("Hello World")
var waitgroup sync.WaitGroup
waitgroup.Add(1)
go myFunc(&waitgroup)
waitgroup.Wait()
fmt.Println("main goroutine done")}
结果如下:
Hello World
hello my goroutine
main goroutine done
还有一点需要特别注意的是myFunc()中使用指针类型的*sync.WaitGroup作为参数,这里不能使用值类型的sync.WaitGroup作为参数,因为这意味着每个goroutine都拷贝一份waitgroup,每个goroutine都使用自己的waitgroup。这显然是不合理的,多个goroutine应该共享一个waitgroup,才能知道这多个goroutine都完成了。实际上,如果使用值类型的参数,main goroutine将会永久阻塞而导致产生死锁。
我们也可以使用匿名函数实现相同功能。对于协程内部业务不复杂,匿名函数会让程序更简洁:
package main
import (
"fmt"
"sync"
)
func main() {
fmt.Println("Hello World")
var waitgroup sync.WaitGroup
waitgroup.Add(1)
go func() {
fmt.Println("hello my goroutine")
waitgroup.Done()
}()
waitgroup.Wait()
fmt.Println("main goroutine done")
}