优雅地等待子协程执行完毕
goroutine
模拟了线程级别的返场的能力,但它的执行需要主协程给机会。一般的作法用sleep,chan
阻塞,看起来让人不爽,本文介绍sync.WaitGroup
类型结合defer
的特性,给出优雅的解决方案。
缘起
下面这段代码众所周知不会打招呼
....
func main(){
go sayHi(){
fmt.Println("say hello......")
}()
fmt.Println("main groutine....")
}
等待
上述代码不会讲hello
,在于主协程main
无等待(阻塞),子协程sayHi
没有露脸的机会。
为了让子协程sayHai上场,通常在主协程末了加这么一句,让它睡会儿
time.Sleep(1e9)
但想想不科学,如果子协程在它睡期间,没能完成任务,超时了,子协程仍然打不了招呼。又或者子协程完成了,主协程还在睡,岂不是主线程不作为?睡的时间多久怎样才合理...
通道
用通道可解决阻塞时间合理性质疑。
若启用了多个子协程,可以这样实现主协程等待子协程执行完毕并退出的:声明一个和子协程数量一致的通道数组,然后为每个子协程分配一个通道元素,在子协程执行完毕时向对应的通道发送数据;然后在主协程中,依次读取这些通道接收子协程发送的数据,只有所有通道都接收到数据才会退出主协程。
代码看起来像这样
chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
go add(1, i, chs[i])
}
for _, ch := range chs {
<- ch
}
感觉这样的实现有点蹩脚,不够优雅,于是 sync.waitGroup
入场了
WaitGroup 类型
sync.WaitGroup
类型是并发安全的,提供了以下三个方法:
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
Add
:WaitGroup 类型有一个计数器,默认值是0,通常通过个方法来标记需要等待的子协程数量Done
:当某个子协程执行完毕后,可以通过 Done 方法标记已完成,常用 defer 语句来调用Wait
阻塞当前协程,直到对应WaitGroup
类型实例的计数器值归零
代码
至此,相信你已经明白了,可组合使用 sync.WaitGroup
类型提供的方法,来替代之前通道中等待子协程执行完毕。
优雅等待子协程执行完毕代码如下
package main
import (
"fmt"
"sync"
)
func add_num(a, b int, done func()) {
defer done()
c := a + b
fmt.Printf("%d + %d = %d\n", a, b, c)
}
func main() {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go add_num(i, 1, wg.Done)
}
wg.Wait()
}
让当前协程永久阻塞
其实runtime 运行时默认是只开启一个逻辑处理器,如果不想用通道,也不阻塞,可以考虑设置多个逻辑处理器。或者干脆用select让当前主协程永久阻塞, 像下面这样
package main
import "runtime"
func DoSomething() {
for {
// 做点什么...
runtime.Gosched() // 防止本协程霸占CPU不放
}
}
func main() {
go DoSomething()
go DoSomething()
select{}
}
本作品采用《CC 协议》,转载必须注明作者和本文链接