概述
很多人选择 Go 语言的原因之一是因为 Go 语言拥有很方便的并发实现,例如原生的 goroutine 和 channel,让协程开发变得更加的容易。但是,因为门槛变低了,所以使用的方式很多时候看上去都是很 naive 的,这不仅仅会影响代码的美观度,其实更重要的是可能会出现隐藏得很深的坑。但是,goroutine 的使用方式不受限制,同时控制的方法也很多,所以很难直接给出一种方式吃遍天下,本文我尝试写几种常用的实践供参考,以便在遇到真实问题的时候有利于选择。
Cancel Context
例如,我尝试写一段 DEMO,我的目的是希望有几个不同的 goroutine 在输出,然后,当我不想让他们输出的时候就按下 “Ctrl + C” 关闭,那么简单的代码可能会这么写:
但是机智如你,肯定知道这段代码是不符合要求的,所以我改了一下:
这里的区别就是我加了一个 ctx,用于控制 goroutine 的生命周期,可以发现,这段代码从易用性和维护性上都还可以,但是,这段代码也暴露了一些问题,第一个是 context 的作用主要还在于继承,如果是平级之间的 goroutine 要想互相关联会比较麻烦,例如 routine-01 打印 5 次之后就所有的 routine 退出这类需求比较难;第二个就是对于网络变成,context 也比较难以适应,例如你开启一个可一个 goroutine 用于监听端口的同时,还要关注是否被按下了 Ctrl + C。
改进 Cancel Context
对于这个 context 不适合的场景,就需要使用另外一种方式了,其实关键就是我们需要关注两个 block 的消息,所以为什么不套多一层 goroutine 呢,于是乎就有了这种操作:
// created by: https://liqiang.io
func errRoutine(ctx context.Context, name string, tickCount int) (err error) {
var count = 0
tick := time.Tick(time.Second)
for {
select {
case <-tick:
if count > tickCount {
return nil
}
count++
fmt.Printf("i am simpleRoutine: %s\n", name)
case <-ctx.Done():
fmt.Printf("%s ready to exit.\n", name)
return
}
}
}
// created by: https://liqiang.io
func main() {
finish := make(chan bool, 3)
ctx, cancel := context.WithCancel(context.Background())
newRoutine := func(ctx context.Context, name string, tickCount int) {
ctx2, cancel2 := context.WithCancel(context.Background())
end := make(chan bool, 3)
go func() {
errRoutine(ctx2, name, tickCount)
log.Printf("[D] cancel2.")
end <- true
}()
select {
case <-ctx.Done():
cancel2()
return
case <-end:
cancel2()
return
}
}
go func() {
newRoutine(ctx, "simpleRoutine-01", 2)
finish <- true
}()
go func() {
newRoutine(ctx, "simpleRoutine-02", 100)
finish <- true
}()
go func() {
newRoutine(ctx, "simpleRoutine-03", 100)
finish <- true
}()
go func(cancel context.CancelFunc) {
WaitForCtrlC()
cancel()
}(cancel)
<-finish
cancel()
}
这段代码可能写得有点绕,但是呢,思路是很明确的,那就是这里在等待两个 block 的信息,任何一个有信号了,另外一个都得放弃,并且退出,因为这是一个固定的模式,所以如果借鉴别人写好的库:run的话,整个事情就变得简单了,可以这么写:
这样,整个流程就变得比较清晰和简单了,同时,你也可以写更少的代码。
小结
本文简单介绍了两个常用的 Go 语言 goroutine 的使用模式,但是,很显然丰富度还是不足的,至少还有 errgroup 和 tomb 没有介绍,但是无妨,这篇文章就当一个引子,希望对大家有所帮助。