浅谈errgroup的使用以及源码分析
本文讲解的是
golang.org/x/sync
这个包中的errgroup
1、errgroup 的基础介绍
学习过 Go 的朋友都知道 Go 实现并发编程是比较容易的事情,只需要使用go
关键字就可以开启一个 goroutine。那对于并发场景中,如何实现goroutine
的协调控制呢?常见的一种方式是使用sync.WaitGroup
来进行协调控制。
使用过sync.WaitGroup
的朋友知道,sync.WaitGroup
虽然可以实现协调控制,但是不能传递错误,那该如何解决呢?聪明的你可能马上想到使用 chan 或者是 context
来传递错误,确实是可以的。那接下来,我们一起看看官方是怎么实现上面的需求的呢?
1.1 errgroup的安装
安装命令:
go get golang.org/x/sync
//下面的案例是基于v0.1.0 演示的
go get golang.org/x/sync@v0.1.0
1.2 errgroup的基础例子
这里我们需要请求3个url来获取数据,假设请求url2时报错,url3耗时比较久,需要等一秒。
package main
import (
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"strings"
"time"
)
func main() {
queryUrls := map[string]string{
"url1": "http://localhost/url1",
"url2": "http://localhost/url2",
"url3": "http://localhost/url3",
}
var eg errgroup.Group
var results []string
for _, url := range queryUrls {
url := url
eg.Go(func() error {
result, err := query(url)
if err != nil {
return err
}
results = append(results, fmt.Sprintf("url:%s -- ret: %v", url, result))
return nil
})
}
// group 的wait方法,等待上面的 eg.Go 的协程执行完成,并且可以接受错误
err := eg.Wait()
if err != nil {
fmt.Println("eg.Wait error:", err)
return
}
for k, v := range results {
fmt.Printf("%v ---> %v\n", k, v)
}
}
func query(url string) (ret string, err error) {
// 假设这里是发送请求,获取数据
if strings.Contains(url, "url2") {
// 假设请求 url2 时出现错误
fmt.Printf("请求 %s 中....\n", url)
return "", errors.New("请求超时")
} else if strings.Contains(url, "url3") {
// 假设 请求 url3 需要1秒
time.Sleep(time.Second*1)
}
fmt.Printf("请求 %s 中....\n", url)
return "success", nil
}
执行结果:
请求 http://localhost/url2 中....
请求 http://localhost/url1 中....
请求 http://localhost/url3 中....
eg.Wait error: 请求超时
果然,当其中一个goroutine
出现错误时,会把goroutine
中的错误传递出来。
我们自己运行一下上面的代码就会发现这样一个问题,请求 url2 出错了,但是依旧在请求 url3 。因为我们需要聚合 url1、url2、url3 的结果,所以当其中一个出现问题时,我们是可以做一个优化的,就是当其中一个出现错误时,取消还在执行的任务,直接返回结果,不用等待任务执行结果。
那应该如何做呢?
这里假设 url1 执行1秒,url2 执行报错,url3执行3秒。所以当url2报错后,就不用等url3执行结束就可以返回了。
package main
import (
"context"
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"strings"
"time"
)
func main() {
queryUrls := map[string]string{
"url1": "http://localhost/url1",
"url2": "http://localhost/url2",
"url3": "http://localhost/url3",
}
var results []string
ctx, cancel := context.WithCancel(context.Background())
eg, errCtx := errgroup.WithContext(ctx)
for _, url := range queryUrls {
url := url
eg.Go(func() error {
result, err := query(errCtx, url)
if err != nil {
//其实这里不用手动取消,看完源码就知道为啥了
cancel()
return err
}
results = append(results, fmt.Sprintf("url:%s -- ret: %v", url, result))
return nil
})
}
err := eg.Wait()
if err != nil {
fmt.Println("eg.Wait error:", err)
return
}
for k, v := range results {
fmt.Printf("%v ---> %v\n", k, v)
}
}
func query(errCtx context.Context, url string) (ret string, err error) {
fmt.Printf("请求 %s 开始....\n", url)
// 假设这里是发送请求,获取数据
if strings.Contains(url, "url2") {
// 假设请求 url2 时出现错误
time.Sleep(time.Second*2)
return "", errors.New("请求出错")
} else if strings.Contains(url, "url3") {
// 假设 请求 url3 需要1秒
select {
case <- errCtx.Done():
ret, err = "", errors.New("请求3被取消")
return
case <- time.After(time.Second*3):
fmt.Printf("请求 %s 结束....\n", url)
return "success3", nil
}
} else {
select {
case <- errCtx.Done():
ret, err = "", errors.New("请求1被取消")
return
case <- time.After(time.Second):
fmt.Printf("请求 %s 结束....\n", url)
return "success1", nil
}
}
}
执行结果:
请求 http://localhost/url2 开始....
请求 http://localhost/url3 开始....
请求 http://localhost/url1 开始....
请求 http://localhost/url1 结束....
eg.Wait error: 请求出错
2、errgroup源码分析
看了上面的例子,我们对errgroup
有了一定了解,接下来,我们一起看看errgroup
做了那些封装。
2.1 errgroup.Group
errgroup.Group
源码如下:
// A Group is a collection of goroutines working on subtasks that are part of
// the same overall task.
//
// A zero Group is valid, has no limit on the number of active goroutines,
// and does not cancel on error.
type Group struct {
// context 的 cancel 方法
cancel func()
wg sync.WaitGroup
//传递信号的通道,这里主要是用于控制并发创建 goroutine 的数量
//通过 SetLimit 设置过后,同时创建的goroutine 最大数量为n
sem chan token
// 保证只接受一次错误
errOnce sync.Once
// 最先返回的错误
err error
}
看结构体中的内容,发现比原生的sync.WaitGroup
多了下面的内容:
cancel func()
sem chan token
errOnce sync.Once
err error
2.2 WithContext 方法
// WithContext returns a new Group and an associated Context derived from ctx.
//
// The derived Context is canceled the first time a function passed to Go
// returns a non-nil error or the first time Wait returns, whichever occurs
// first.
func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return &Group{cancel: cancel}, ctx
}
方法逻辑还是比较简单的,主要做了两件事:
- 使用
context
的WithCancel()
方法创建一个可取消的Context
- 将
context.WithCancel(ctx)
创建的cancel
赋值给 Group中的cancel
2.3 Go
1.2 最后一个例子说,不用手动去执行 cancel 的原因就在这里。
g.cancel() //这里就是为啥不用手动执行 cancel的原因
// Go calls the given function in a new goroutine.
// It blocks until the new goroutine can be added without the number of
// active goroutines in the group exceeding the configured limit.
//
// The first call to return a non-nil error cancels the group's context, if the
// group was created by calling WithContext. The error will be returned by Wait.
func (g *Group) Go(f func() error) {
if g.sem != nil {
//往 sem 通道中发送空结构体,控制并发创建 goroutine 的数量
g.sem <- token{}
}
g.wg.Add(1)
go func() {
// done()函数的逻辑就是当 f 执行完后,从 sem 取一条数据,并且 g.wg.Done()
defer g.done()
if err := f(); err != nil {
g.errOnce.Do(func() { // 这里就是确保 g.err 只被赋值一次
g.err = err
if g.cancel != nil {
g.cancel() //这里就是为啥不用手动执行 cancel的原因
}
})
}
}()
}
2.4 TryGo
看注释,知道此函数的逻辑是:当正在执行的goroutine数量小于通过SetLimit()
设置的数量时,可以启动成功,返回 true,否则启动失败,返回false。
// TryGo calls the given function in a new goroutine only if the number of
// active goroutines in the group is currently below the configured limit.
//
// The return value reports whether the goroutine was started.
func (g *Group) TryGo(f func() error) bool {
if g.sem != nil {
select {
case g.sem <- token{}: // 当g.sem的缓冲区满了过后,就会执行default,也代表着未启动成功
// Note: this allows barging iff channels in general allow barging.
default:
return false
}
}
//----主要看上面的逻辑,下面的逻辑和Go中的一样-------
g.wg.Add(1)
go func() {
defer g.done()
if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
return true
}
2.5 Wait
代码逻辑很简单,这里主要注意这里:
//我看这里的时候,有点疑惑,为啥这里会去调用 cancel()方法呢?
//这里是为了代码的健壮性,用 context.WithCancel() 创建得到的 cancel,在代码执行完毕之前取消是一个好习惯
g.cancel()
// Wait blocks until all function calls from the Go method have returned, then
// returns the first non-nil error (if any) from them.
func (g *Group) Wait() error {
g.wg.Wait() //通过 g.wg.Wait() 阻塞等待所有的 goroutine 执行完
if g.cancel != nil {
//我看这里的时候,有点疑惑,为啥这里会去调用 cancel()方法呢?
//这里是为了代码的健壮性,用 context.WithCancel() 创建得到的 cancel,在代码执行完毕之前取消是一个好习惯
g.cancel()
}
return g.err
}
2.6 SetLimit
看代码的注释,我们知道:SetLimit
的逻辑主要是限制同时执行的 goroutines 的数量为n,当n小于0时,没有限制。如果有运行的 goroutine,调用此方法会报错。
// SetLimit limits the number of active goroutines in this group to at most n.
// A negative value indicates no limit.
//
// Any subsequent call to the Go method will block until it can add an active
// goroutine without exceeding the configured limit.
//
// The limit must not be modified while any goroutines in the group are active.
func (g *Group) SetLimit(n int) {
if n < 0 {
g.sem = nil
return
}
if len(g.sem) != 0 {
panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem)))
}
g.sem = make(chan token, n)
}
3、errgroup 容易忽视的坑
这个坑是看别人的记录看到的,对errgroup
不太熟悉时,是不小心确实容易掉进去,所以摘抄了过来,如果侵权,请联系删除,谢谢!
原文链接:并发编程包之 errgroup
需求:
开启多个
Goroutine
去缓存中设置数据,同时开启一个Goroutine
去异步写日志,很快我的代码就写出来了:
package main
import (
"context"
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"time"
)
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
是errgroup.WithContext
方法返回的一个带取消的ctx
,我们把这个ctx
当作父context
传入WriteChangeLog
方法中了,如果errGroup
取消了,也会导致上下文的context
都取消了,所以WriteChangelog
方法就一直执行不到。
这个点是我们在日常开发中想不到的,所以需要注意一下~。
解决方法:
解决方法就是在 go WriteChangeLog(context.Background()) 传入新的ctx
参考资料:
上面这个案例中讲了一个容易忽视的坑,大家可以看看
浅谈errgroup的使用以及源码分析的更多相关文章
- 浅谈ZooKeeper基本原理与源码分析
最近一直有小伙伴私信我,问一些关于Zookeeper的知识,下边关于的Zookeeper的知识整理了一下,一起学习一下. 看完本文对于Zookeeper想深入全面了解的读者朋友们,小编这里整理了一份更 ...
- mybatis缓存源码分析之浅谈缓存设计
本文是关于mybatis缓存模块设计的读后感,关于缓存的思考,关于mybatis的缓存源码详细分析在另一篇文章:https://www.cnblogs.com/gmt-hao/p/12448896.h ...
- 集合之HashMap(含JDK1.8源码分析)
一.前言 之前的List,讲了ArrayList.LinkedList,反映的是两种思想: (1)ArrayList以数组形式实现,顺序插入.查找快,插入.删除较慢 (2)LinkedList以链表形 ...
- Spring 源码分析之 bean 依赖注入原理(注入属性)
最近在研究Spring bean 生命周期相关知识点以及源码,所以打算写一篇 Spring bean生命周期相关的文章,但是整理过程中发现涉及的点太多而且又很复杂,很难在一篇文章中把Spri ...
- Django之REST framework源码分析
前言: Django REST framework,是1个基于Django搭建 REST风格API的框架: 1.什么是API呢? API就是访问即可获取数据的url地址,下面是一个最简单的 Djang ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- LinqToDB 源码分析——生成表达式树
当我们知道了Linq查询要用到的数据库信息之后.接下就是生成对应的表达式树.在前面的章节里面笔者就已经介绍过.生成表达式树是事实离不开IQueryable<T>接口.而处理表达式树离不开I ...
- Duilib源码分析(六)整体流程
在<Duilib源码分析(一)整体框架>.<Duilib源码分析(二)控件构造器—CDialogBuilder>以及<Duilib源码分析(三)XML解析器—CMarku ...
- 【集合框架】JDK1.8源码分析之HashMap(一)
一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...
- HashMap实现原理及源码分析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...
随机推荐
- [转帖]使用Linux命令快速查看某一行
原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 当年,我还是Linux菜鸟的时候,就在简历上写着精通Linux命令了,而当面试官问我"如何快速查看 ...
- [转帖] Linux命令拾遗-网络抓包工具
https://www.cnblogs.com/codelogs/p/16060684.html 简介# Linux中有很多抓包工具,如ngrep.tcpdump与tshark等,它们有很多用法与使用 ...
- [转帖]tar、gzip、zip、jar是什么,怎么查看?
https://www.cnblogs.com/codelogs/p/16702759.html 原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 如果你是后 ...
- 源码阅读:VictoriaMetrics中的golang代码优化方法
全文请移步:https://zhuanlan.zhihu.com/p/469239020 或关注我的公众号: 公众号:一本正经的瞎扯
- vue脚手架创建与环境安装
1.安装 Node.jsDownload | Node.js 在这里下载的是最新版,如果要安装以前的版本,页面往下拉找到 Previous Releases-Donloads-下载msi文件. ...
- 批量修改SVN的用户名和密码的尝试
起源 公司规定每6个月需要修改一次密码,否则每天都有邮件和内网提醒.因为邮箱密码和svn等一系列应用绑定,避免每次修改密码后需要手工输入修改多个svn仓库的帐号和密码. PS.同一个前缀的svn不用重 ...
- vim 从嫌弃到依赖(16)——宏
终于到了我第二喜欢的vim功能了(当然了,最喜欢的是.命令).我原本计划在介绍完.命令之后介绍宏,以便让各位小伙伴们能了解到vim对于重复操作进行的强大的优化.但是由于宏本身跟寄存器息息相关,所以还是 ...
- gitee 命令合集(从远程仓库拉取项目到推送项目到远程仓库)
1.配置用户的信息 git config --global user.name '你的用户名' git config --global user.email '你的邮箱' 2.初始化 Git 仓库,生 ...
- P3509 [POI2010] ZAB-Frog 题解
题目链接:ZAB-Frog 基于一个根据距离第 \(k\) 大的事实: 容易知道,对于红色的点而言,与它相近最近的 \(k\) 个点是连续的.而第 \(k\) 远的要么是最左侧要么是最右侧.而我们注意 ...
- CF678F Lena and Queries题解
题目链接:CF 或者 洛谷 可以看到查询和插入就是李超线段树的基本操作,但在原有基础上多了一个删除操作,李超线段树不支持删除操作,但支持可撤销和可持久化,所以我们容易想到外层再套一个线段树分治即可.本 ...