关于golang:Golang并发编程包之-errgroup

原文链接:并发编程包之 errgroup

前言

哈喽,大家好,我是asong,明天给大家介绍一个并发编程包errgroup,其实这个包就是对sync.waitGroup的封装。咱们在之前的文章—— 源码分析sync.WaitGroup(文末思考题你能解释一下吗?),从源码层面剖析了sync.WaitGroup的实现,应用waitGroup能够实现一个goroutine期待一组goroutine干活完结,更好的实现了工作同步,然而waitGroup却无奈返回谬误,当一组Goroutine中的某个goroutine出错时,咱们是无奈感知到的,所以errGroupwaitGroup进行了一层封装,封装代码仅仅不到50行,上面咱们就来看一看他是如何封装的?

errGroup如何应用

老规矩,咱们先看一下errGroup是如何应用的,后面吹了这么久,先来验验货;

以下来自官网文档的例子:

var (
    Web   = fakeSearch("web")
    Image = fakeSearch("image")
    Video = fakeSearch("video")
)

type Result string
type Search func(ctx context.Context, query string) (Result, error)

func fakeSearch(kind string) Search {
    return func(_ context.Context, query string) (Result, error) {
        return Result(fmt.Sprintf("%s result for %q", kind, query)), nil
    }
}

func main() {
    Google := func(ctx context.Context, query string) ([]Result, error) {
        g, ctx := errgroup.WithContext(ctx)

        searches := []Search{Web, Image, Video}
        results := make([]Result, len(searches))
        for i, search := range searches {
            i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines
            g.Go(func() error {
                result, err := search(ctx, query)
                if err == nil {
                    results[i] = result
                }
                return err
            })
        }
        if err := g.Wait(); err != nil {
            return nil, err
        }
        return results, nil
    }

    results, err := Google(context.Background(), "golang")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    for _, result := range results {
        fmt.Println(result)
    }

}

下面这个例子来自官网文档,代码量有点多,然而外围次要是在Google这个闭包中,首先咱们应用errgroup.WithContext创立一个errGroup对象和ctx对象,而后咱们间接调用errGroup对象的Go办法就能够启动一个协程了,Go办法中曾经封装了waitGroup的管制操作,不须要咱们手动增加了,最初咱们调用Wait办法,其实就是调用了waitGroup办法。这个包不仅缩小了咱们的代码量,而且还减少了错误处理,对于一些业务能够更好的进行并发解决。

赏析errGroup

数据结构

咱们先看一下Group的数据结构:

type Group struct {
    cancel func() // 这个存的是context的cancel办法

    wg sync.WaitGroup // 封装sync.WaitGroup

    errOnce sync.Once // 保障只承受一次谬误
    err     error // 保留第一个返回的谬误
}

办法解析

func WithContext(ctx context.Context) (*Group, context.Context)
func (g *Group) Go(f func() error)
func (g *Group) Wait() error

errGroup总共只有三个办法:

  • WithContext办法
func WithContext(ctx context.Context) (*Group, context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    return &Group{cancel: cancel}, ctx
}

这个办法只有两步:

  • 应用contextWithCancel()办法创立一个可勾销的Context
  • 创立cancel()办法赋值给Group对象
  • Go办法
func (g *Group) Go(f func() error) {
    g.wg.Add(1)

    go func() {
        defer g.wg.Done()

        if err := f(); err != nil {
            g.errOnce.Do(func() {
                g.err = err
                if g.cancel != nil {
                    g.cancel()
                }
            })
        }
    }()
}

Go办法中运行步骤如下:

  • 执行Add()办法减少一个计数器
  • 开启一个协程,运行咱们传入的函数f,应用waitGroupDone()办法管制是否完结
  • 如果有一个函数f运行出错了,咱们把它保存起来,如果有cancel()办法,则执行cancel()勾销其余goroutine

这里大家应该会好奇为什么应用errOnce,也就是sync.Once,这里的目标就是保障获取到第一个出错的信息,防止被前面的Goroutine的谬误笼罩。

  • wait办法
func (g *Group) Wait() error {
    g.wg.Wait()
    if g.cancel != nil {
        g.cancel()
    }
    return g.err
}

总结一下wait办法的执行逻辑:

  • 调用waitGroupWait()期待一组Goroutine的运行完结
  • 这里为了保障代码的健壮性,如果后面赋值了cancel,要执行cancel()办法
  • 返回错误信息,如果有goroutine呈现了谬误才会有值

小结

到这里咱们就剖析完了errGroup包,总共就1个构造体和3个办法,了解起来还是比较简单的,针对下面的知识点咱们做一个小结:

  • 咱们能够应用withContext办法创立一个可勾销的Group,也能够间接应用一个零值的Groupnew一个Group,不过间接应用零值的Groupnew进去的Group呈现谬误之后就不能取消其余Goroutine了。
  • 如果多个Goroutine呈现谬误,咱们只会获取到第一个出错的Goroutine的错误信息,晚于第一个出错的Goroutine的错误信息将不会被感知到。
  • errGroup中没有做panic解决,咱们在Go办法中传入func() error办法时要保障程序的健壮性

踩坑日记

应用errGroup也并不是一番风顺的,我之前在我的项目中应用errGroup就呈现了一个BUG,把它分享进去,防止踩坑。

这个需要是这样的(并不是实在业务场景,由asong虚构的):开启多个Goroutine去缓存中设置数据,同时开启一个Goroutine去异步写日志,很快我的代码就写进去了:

func main()  {
    g, ctx := errgroup.WithContext(context.Background())

    // 独自开一个协程去做其余的事件,不参加waitGroup
    go WriteChangeLog(ctx)

    for i:=0 ; i< 3; i++{
        g.Go(func() error {
            return errors.New("拜访redis失败\n")
        })
    }
    if err := g.Wait();err != nil{
        fmt.Printf("appear error and err is %s",err.Error())
    }
    time.Sleep(1 * time.Second)
}

func WriteChangeLog(ctx context.Context) error {
    select {
    case <- ctx.Done():
        return nil
    case <- time.After(time.Millisecond * 50):
        fmt.Println("write changelog")
    }
    return nil
}
// 运行后果
appear error and err is 拜访redis失败

代码没啥问题吧,然而日志始终没有写入,排查了良久,终于找到问题起因。起因就是这个ctx

因为这个ctxWithContext办法返回的一个带勾销的ctx,咱们把这个ctx当作父context传入WriteChangeLog办法中了,如果errGroup勾销了,也会导致上下文的context都勾销了,所以WriteChangelog办法就始终执行不到。

这个点是咱们在日常开发中想不到的,所以须要留神一下~。

总结

因为最近看很多敌人都不晓得这个库,所以明天就把他分享进去了,封装代码仅仅不到50行,真的是很厉害,如果让你来封装,你能封装的更好吗?

欢送关注公众号:【Golang梦工厂】

举荐往期文章:

  • 学习channel设计:从入门到放弃
  • 编程模式之Go如何实现装璜器
  • Go语言中new和make你应用哪个来分配内存?
  • 源码分析panic与recover,看不懂你打我好了!
  • 空构造体引发的大型打脸现场
  • [面试官:你能聊聊string和[]byte的转换吗?](https://mp.weixin.qq.com/s/jz…
  • 面试官:两个nil比拟后果是什么?
  • 面试官:你能用Go写段代码判断以后零碎的存储形式吗?
  • 赏析Singleflight设计

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理