前言
最近遇到了一个很有意思的问题, 感觉值得写一篇博客来记录一下, 也在大家遇到这种问题的时候可以有个参考; 下面这段代码大家都不陌生吧, 一个简单的多go
程处理, 大家可以看看有没有什么问题
func handle() {
var (
errCh = make(chan error, 1)
doneCh = make(chan struct{})
records = make([]string, 100)
gp = gopool.NewGoPool(10) // go程池, 只允许开10个go程
)
for _, v := range records {
if len(errCh) > 0 {
break
}
gp.Add()
go func(v string) {
defer gp.Done()
// handles
// check(err)
errCh <- fmt.Errorf("err")
}(v)
}
go func() {
gp.Wait()
doneCh <- struct{}{}
}()
select {
case <-doneCh:
fmt.Println("done")
case <-errCh:
fmt.Println("err")
}
}
问题
其实这段代码里有很大的安全隐患, 列举一下:
- 如果
for
循环不判断errCh
, 在100个循环完之前产生11个err
,errCh
插入不进去,for
循环就会直接死锁, 走不到下面的select
; - 虽然判断了
errCh
保证可以终止for
循环, 但是其他go程
产生错误也会死锁, 并且每次运行这个函数都有可能产生死锁go程
, 会产生Goroutine Leak
; - 最后
select
不能完全保证doneCh
和errCh
哪个优先监听到;
那么有问题就要有对应的解决思路及方案, 下面给出几个:
方案一 扩充errCh
的大小;
点评
最简单粗暴的方案, 但是会造成内存浪费;
实现
errCh = make(chan error, len(records))
方案二 使用context.WithCancel
;
点评
context
替换errCh
作为监听err
, cancel()
可以多次执行, 不会阻塞; 比思路一优雅一些, 但是代码会比较冗余;
实现
func handle() {
var (
ctx, cancel = context.WithCancel(context.Background())
doneCh = make(chan struct{})
records = make([]string, 100)
gp = gopool.NewGoPool(10) // go程池, 只允许开10个go程
)
go func() {
for _, v := range records {
select {
case <-ctx.Done():
return
default:
}
gp.Add()
go func(v string) {
defer gp.Done()
// handles
// check(err)
cancel()
}(v)
}
}()
go func() {
gp.Wait()
time.Sleep(1 * time.Millisecond)
select {
case <-ctx.Done():
return
default:
}
doneCh <- struct{}{}
}()
select {
case <-doneCh:
fmt.Println("done")
case <-ctx.Done():
fmt.Println("err")
}
}
方案三 使用sync.Once
点评
使用 Once
来限制 插入errCh
操作只执行一次; 目前最优雅的思路, 代码改动也最少;
实现
func handle() {
var (
errCh = make(chan error, 1)
doneCh = make(chan struct{})
records = make([]string, 100)
gp = gopool.NewGoPool(10) // go程池, 只允许开10个go程
_once = new(sync.Once)
)
for _, v := range records {
if len(errCh) > 0 {
break
}
gp.Add()
go func(v string) {
defer gp.Done()
// handles
// check(err)
_once.Do(func() {
errCh <- fmt.Errorf("err")
})
}(v)
}
go func() {
gp.Wait()
time.Sleep(1 * time.Millisecond) // 优先监听errCh
doneCh <- struct{}{}
}()
select {
case <-doneCh:
fmt.Println("done")
case <-errCh:
fmt.Println("err")
}
}
结束语
大家如果有更好的思路/方案可以在评论区/私信给我, 共同学习共同进步;
我自己写了一个Go开箱即用的开源项目, 里面封装了常用的一些组件, git clone
下来就可以直接进行API开发, 有兴趣的可以给个Star
, 会一直持续维护;