新手玩Go协程的一些小坑
大家好,我是一根甜苦瓜,今天来分享新手玩Go协程的一些小坑
1. 问题代码
我们先来看下面的三段代码,看能不能看出问题,如果能看出问题并找到解决方案,那么恭喜你,入行了,后面的文章不用读了。代码如下
// 代码块一
func handleV1(items []Request){for _,item := range items{go procesItem(item)}
}// 代码块2
func handleV2(items chan Request){for item := range items{go procesItem(item)}
}
// 代码块3
func handleV3(items []Request) error {for _,item := range items{go func(i item){if err := processItem(i);err != nil{}}(item)}return nil
}
2 逐个分析
下面来逐个分析上面代码的问题在哪,以及解决方案
2.1 协程泄漏
func handleV1(items []Request){for _,item := range items{go procesItem(item)}// 上面的协程还没全部处理完,函数已经返回了
}
我们先来分析代码块一,他存在明显的协程泄漏问题!也就是说handleV1
函数返回时,没有等待所有procesItem
完成,而这些groutine
还在运行。他的弊端如下:
- 资源泄露,和不可控的退出,这是最大的问题。
- 在服务关闭或者任务取消时,这些groutine无法优雅停止
- 可能会导致内存泄漏
解决方案:最简单的就是用sync.Waitgroup + context
func handleV1(ctx context.Context,items []Request){var wg sync.WaitGroupfor _,item := range items{wg.Add(1)go func(r Request) {defer wg.Done()select {case <-ctx.Done():return // 上下文取消时优雅退出default:procesItem(r)}}(req)}wg.Wait() // 等待所有任务完成
}
上面的代码主要用了sync.WaitGroup方法,具体来说就是每启动一个groutine就执行一次wg.Add(1)方法,最后在返回之前调用 wg.Wait()方法,他会等待所有groutine执行结束。同时使用context来监听上下文的级联信号,这是一个很常见的小技巧。
2.2 无限制创建协程
// 代码块2
func handleV2(items chan Request){for item := range items{go procesItem(item)}
}
我们再来看看代码块二,他最大的问题是协程数量不可控,当request来得太快,会创建不可估量的groutine。虽然goroutine是很轻量的,但是如果不限制数量还是会出现CPU飙升,内存爆炸,GC压力过等问题。所以一定要对协程数量进行限制。
解决方案:常用的方法是用协程池来解决。对于协程池,业界也有很好的开源工具比如:https://github.com/panjf2000/ants,这里我们自己手写一个简单的协程池。
func handleHighTraffic(items chan Request) {const workerCount = 10var wg sync.WaitGroup// 启动固定数量的 workerfor i := 0; i < workerCount; i++ {wg.Add(1)go func() {defer wg.Done()for req := range items {procesItem(req)}}()}wg.Wait()
}
当然,实际生产环境还是建议搭建使用功能更强大的协程池工具,比如https://github.com/panjf2000/ants
2.3 错误处理困难(异步执行错误无法返回)
func handleV3(items []Request) error {for _,item := range items{go func(i item){if err := processItem(i);err != nil{}}(item)}return nil
}
上面代码的问题也是初学者懊恼的一个问题,如果多个groutine执行期间,某一些协程出现了error,那么主逻辑应该如何捕获这些error?肯定不能像上面那样直接return nil,那么应该如何汇总错误,特别是在批量任务的时候,如何判断成功或失败呢?
解决方案:errgroup(Go官方推荐的库)
Go 提供了 golang.org/x/sync/errgroup,简洁优雅地解决此类问题。
import "golang.org/x/sync/errgroup"func batchProcess(ctx context.Context, items []Request) error {g, ctx := errgroup.WithContext(ctx)for _, item := range items {i := itemg.Go(func() error {return processItem(i)})}// 等待所有任务完成,并返回第一个错误return g.Wait()
}
errgroup是使用十分广泛的一个包,用来自动等待所有任务的同时,还能抓到第一个错误,我们无需手动维护sync.Waitgroup。
3. 总结
上面的手写的协程泄漏和协程池都是十分简陋的,大家可以去搜索一下一些优秀的开源项目,看看其源码,对于我们理解协程更有帮助。实际开发过程中我们应该时刻注意
- 任何groutine都必须有退出条件
- 所有groutine最好有数量控制