实时系统降低延时的利器

在日常工作当中,如果接口涉及到一些向下游服务请求多次或者多个服务,那么这个接口的响应时长就会飙升,如果是一些调用时长比较高的接口的话,比如说AI相关接口,那么服务暴露出去的接口时长会很高,那么就需要并发等待技术-WaitGroup类型和errgroup包

WaitGroup类型

当面对这种场景时,常规解决方法是用 Golang 的基础并发类型 WaitGroup。WaitGroup 的作用是阻塞等待多个并发任务执行完成。WaitGroup 类型主要包含下面几个方法。

scss 复制代码
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
  • 第一个方法就是Add方法,主要的作用就是添加任务数的数量,告诉WaitGroup有多少数量
  • 第二个方法是Done任务结束的标识,告诉WaitGroup已经有一个任务完成了,一般使用defer wg.Done()
  • 第三个方法Wait方法,主要作用用来阻塞主协程

我们举一个例子

  • 1.首先创建一个sync.WaitGroup,用来创建并发等待任务
  • 2.循环遍历数组,往WaitGroup添加任务数
  • 3.当协程结束告诉WaitGroup结束一个任务
  • 4.在主协程阻塞等待所有的协程结束

WaitGroup已经能够实现并发等待的功能,但是其实在一些错误的处理上,以及对协程的控制上是有问题的,比如某一个协程中处理任务的时候出现了错误,主协程难以进行处理并且对于正在运行的协程进行处理

errgroup

errgroup包的核心是group类型,主要是对WaitGroup类型的封装,在并发等待的基础之上,它额外提供了一系列实用的扩展功能

主协程获取子协程的错误信息

go 复制代码
func TestErrHandle(t *testing.T) {
    results := make([]string, 5)
    results[0] = "1"
    results[1] = "2"
    results[2] = "3"
    results[3] = "4"
    results[4] = "5"
    // 创建Group类型
    g := new(errgroup.Group)
    for _, v := range results {
       // Launch a goroutine to fetch the URL.
       // 调用Go方法
       g.Go(func() error {
          // Fetch the URL.
          if v == "6" {
             return errors.New("err num")
          }
          return nil
       })
    }
    // Wait for all HTTP fetches to complete.
    // 等待所有任务执行完成,并对错误进行处理
    if err := g.Wait(); err != nil {
       fmt.Println("Get Num error.")
    }
}
  • 在g.Wait()当中能够获取到子协程的错误信息进行处理

能够中止并发任务的运行

go 复制代码
func TestCancel(t *testing.T) {
    num := []int{1, 2, 3, 4}
    // 用WithContext函数创建Group对象
    eg, ctx := errgroup.WithContext(context.Background())
    for _, value := range num {
       // 调用Go方法
       eg.Go(func() error {
          select {
          case <-ctx.Done(): // select-done模式取消运行
             return errors.New("task is cancelled")
          default:
             if value >= 5 {
                return errors.New("num is error")
             }
             // Fetch the URL.
             fmt.Println(value)
             return nil
          }
       })
    }
    // Wait for all HTTP fetches to complete.
    // 等待所有任务执行完成,并对错误进行处理
    if err := eg.Wait(); err != nil {
       fmt.Println("fail to get num.")
    }
}
  • 除了错误处理的功能,errgroup还具有的是中止任务的功能,如果有一个并发任务失败,就能够去中止其他的并发任务

控制协程并发执行的最大并发数

errgroup包能够控制同时并发执行的最大协程数 ,核心方法就是Setlimit方法,如果我们使用Setlimit方法设置了最大协程数,当运行的协程达到最大数量之后,就会阻塞新协程的创建,直到有协程运行完,才能创建新的协程

go 复制代码
func TestLimitGNum(t *testing.T) {
    results := make([]string, 5)
    results[0] = "1"
    results[1] = "2"
    results[2] = "3"
    results[3] = "4"
    results[4] = "5"
    // 用WithContext函数创建Group对象
    eg, ctx := errgroup.WithContext(context.Background())
    // 调用SetLimit方法,设置可同时运行的最大协程数
    eg.SetLimit(2)
    for _, value := range results {
       // 调用Go方法
       v := value
       eg.Go(func() error {
          select {
          case <-ctx.Done(): // select-done模式取消运行
             return errors.New("task is cancelled")
          default:
             fmt.Println("v", v)
             return nil
          }
       })
    }
    // Wait for all HTTP fetches to complete.
    // 等待所有任务执行完成,并对错误进行处理
    if err := eg.Wait(); err != nil {
       fmt.Println("Failured fetched all URLs.")
    }
}

errgroup是如何实现的

通过源码来学习设计思想

go 复制代码
type token struct{}

type Group struct {
    cancel func(error) // 这个作用是為了前面說的 WithContext 而來的

    wg sync.WaitGroup // errGroup底层的阻塞等待功能,就是通过WaitGroup实现的

    sem chan token // 用于控制最大运行的协程数

    err     error // 最后在Wait方法中返回的error
    errOnce sync.Once // 用于安全的设置err
}
  • cancel:用来实现上述的WithContext功能来设计的,用来控制取消任务的运行
  • wg:靠WaitGroup来实现阻塞等待的功能
  • sem:用来实现Setlimit函数,控制同时能够运行协程的最大数量
  • err:在Group的Wait方法当中,它会被返回给调用者
  • errOnce是用来保证err变量只设置一次,多个协程一起跑的情况下能够保证并发安全

WithCancel和Setlimit核心方法解读

go 复制代码
func WithContext(ctx context.Context) (*Group, context.Context) {
    ctx, cancel := context.WithCancelCause(ctx)
    return &Group{cancel: cancel}, ctx
}
go 复制代码
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)
}
  • 然后,咱们来看看 Go 方法,这个方法是 Group 类型的核心方法。
go 复制代码
func (g *Group) Go(f func() error) {
    if g.sem != nil {
       g.sem <- token{}
    }

    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(g.err)
             }
          })
       }
    }()
}
scss 复制代码
func (g *Group) done() {
    if g.sem != nil {
       <-g.sem
    }
    g.wg.Done()
}
  • 假如我们调用Setlimit方法,在使用Go方法创建协程的时候,会在第三行对通道塞消息,如果已经满了,那么就会阻塞协程的创建,当协程运行完,在第八行的done方法就会读通道,相当于释放一个协程的位置
  • 第六行和第八行相当于是WaitGroup的Add和Done方法
  • 第10-12行,当传入Go函数出现错误的时候,能够并发安全设置err变量,这个错误最后会传递到Wait方法

Wait方法

go 复制代码
func (g *Group) Wait() error {
    g.wg.Wait()
    if g.cancel != nil {
       g.cancel(g.err)
    }
    return g.err
}
  • 使用Wait来进行阻塞,如果收到了错误信息进行返回g.err,这样就能实现错误处理
相关推荐
徐小黑ACG1 小时前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
战族狼魂3 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
杉之5 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
hycccccch5 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
bobz9656 小时前
k8s 怎么提供虚拟机更好
后端
bobz9656 小时前
nova compute 如何创建 ovs 端口
后端
用键盘当武器的秋刀鱼7 小时前
springBoot统一响应类型3.5.1版本
java·spring boot·后端
Asthenia04127 小时前
从迷宫到公式:为 NFA 构造正规式
后端
Asthenia04128 小时前
像整理玩具一样:DFA 化简和状态等价性
后端
Asthenia04128 小时前
编译原理:打包思维-NFA 怎么变成 DFA
后端