zoukankan      html  css  js  c++  java
  • 八. Go并发编程errGroup

    一. 前言

    了解 sync.WaitGroup的用法都知道

    • 一个 goroutine 需要等待多个 goroutine 完成和多个 goroutine 等待一个 goroutine 干活时都可以解决问题


    WaitGroup 的确是一个很强大的工具,但是使用它相对来说还是有一点小麻烦,

    • 一方面我们需要自己手动调用 Add() 和 Done() 方法,一旦这两个方法有一个多调用或者少调用,最终都有可能导致程序崩溃,所以我们在使用这两个方法的时候要格外小心,确保最终计数器能够达到 0 的状态;
    • 另一方面就是它不能抛出错误给调用者,只要一个 goroutine 出错我们就不再等其他 goroutine 了,减少资源浪费,所以我们只能通过声明多个外部变量的方式(或者声明一个变量然后通过加锁来更新它的值)来分别接收每个协程的 error 才行,就像下面的代码:
      func main() {
       var (
       	wg sync.WaitGroup
       	err1, err2 error  // 通过在外部定义变量用来记录错误
       )
      
       wg.Add(2)
       go func() {
       	defer wg.Done()
       	fmt.Print("task 1")
       	err1 = nil
       }()
       
       go func() {
       	defer wg.Done()
       	fmt.Print("task 2")
       	err2 = fmt.Errorf("task 2 error")
       }()
       wg.Wait()
       
       if err1 != nil || err2 != nil {
       	// TODO
       }
      
       fmt.Print("finish")
      }
      
      

    使用WaitGroup 都不能很好的解决。 所以此时可以使用 ErrGroup 就可以解决问题了。


    二. Errgroup

    Errgroup 是 Golang 官方提供的一个同步扩展库, 代码仓库如下

    它和 WaitGroup 的作用类似,但是它提供了更加丰富的功能以及更低的使用成本:

    1. 和context集成;
    2. 能够对外传播error,可以把子任务的错误传递给Wait 的调用者.

    Errgroup 的代码非常简短,加上注释一共才 66 行,包含一个结构体以及三个对外暴露的方法,接下来就让我们走进源码,来具体看一下它是如何工作的


    2.1 Group

    type Group struct {
        // context 的 cancel 方法
    	cancel func()
    
        // 复用 WaitGroup
    	wg sync.WaitGroup
    
    	// 用来保证只会接受一次错误
    	errOnce sync.Once
        // 保存第一个返回的错误
    	err     error
    }
    

    2.2 WithContext

    func WithContext(ctx context.Context) (*Group, context.Context) {
    	// 使用 contex.WithCancel创建一个可以取消的 context 将 cancel 赋值给 Group 保存起来
    	ctx, cancel := context.WithCancel(ctx)
    	return &Group{cancel: cancel}, ctx
    }
    

    WithContext 就是使用 WithCancel创建一个可以取消的 context 将 cancel 赋值给 Group 保存起来,然后再将 context 返回回去

    注意这里有一个坑,在后面的代码中不要把这个 ctx 当做父 context 又传给下游,因为 errgroup 取消了,这个 context 就没用了,会导致下游复用的时候出错
    Go


    2.3 Go

    Go 方法传入一个 func() error 内部会启动一个 goroutine 去处理

    // Go calls the given function in a new goroutine.
    //
    // The first call to return a non-nil error cancels the group; its error will be
    // returned by Wait.
    func (g *Group) Go(f func() error) {
        // wg.Add(1) 计数器加 1
    	g.wg.Add(1)
    
    	go func() {
    		defer g.wg.Done()
    
    		if err := f(); err != nil {
    		  // 这里使用sync.Once作用,保证传入的 无入参函数执行一次
    		  // 如果有 error,则记录发生的第一个 error
    			g.errOnce.Do(func() {
    				g.err = err
    				if g.cancel != nil {
    					g.cancel()
    				}
    			})
    		}
    	}()
    }
    

    Go 方法其实就类似于 go 关键字,会启动一个协程,然后利用 waitgroup 来控制是否结束,如果有一个非 nil 的 error 出现就会保存起来并且如果有 cancel 就会调用 cancel 取消掉,使 ctx 返


    2.4 Wait

    / 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 {
        // wg.Wait() 等待所有任务执行完毕
    	g.wg.Wait()
    	if g.cancel != nil {
    		g.cancel()
    	}
    	return g.err
    }
    

    Wait 方法其实就是调用 WaitGroup 等待,如果有 cancel 就调用一下


    三. 案例

    3.1 记录错误

    使用 Errgroup,上述代码可以改为下面的样子:

    package main
    
    import (
    	"fmt"
    
    	"golang.org/x/sync/errgroup"
    )
    
    func main() {
    	var eg errgroup.Group
    
    	//匿名函数将会通过GO关键字启动一个协程
    	eg.Go(func() error {
    		fmt.Print("task 1\n")
    		return nil
    	})
    
    	eg.Go(func() error {
    		fmt.Print("task 2\n")
    		return fmt.Errorf("task 2 error")
    	})
    
    	// 使用Wait 等待所有的协程执行完毕后,再进行后面的逻辑,同时可以记录两个协程的错误
    	if err := eg.Wait(); err != nil {
    		fmt.Printf("some error occur: %s\n", err.Error())
    	}
    
    	fmt.Print("over")
    }
    
    

    代码简洁了很多


    3.2 一个协程出错,其他协程终止

    基于 errgroup 实现一个 http server 的启动和关闭 ,以及 linux signal 信号的注册和处理,要保证能够 一个退出,全部注销退出。

    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"os/signal"
    	"syscall"
    	"time"
    
    	"golang.org/x/sync/errgroup"
    )
    
    func main() {
    	g, ctx := errgroup.WithContext(context.Background())
    
    	mux := http.NewServeMux()
    	mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
    		w.Write([]byte("pong"))
    	})
    
    	// 模拟单个服务错误退出
    	serverOut := make(chan struct{})
    	mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
    		serverOut <- struct{}{}
    	})
    
    	server := http.Server{
    		Handler: mux,
    		Addr:    ":8080",
    	}
    
    	// g1
    	// g1 退出了所有的协程都能退出么?
    	// g1 退出后, context 将不再阻塞,g2, g3 都会随之退出
    	// 然后 main 函数中的 g.Wait() 退出,所有协程都会退出
    	g.Go(func() error {
    		return server.ListenAndServe()
    	})
    
    	// g2
    	// g2 退出了所有的协程都能退出么?
    	// g2 退出时,调用了 shutdown,g1 会退出
    	// g2 退出后, context 将不再阻塞,g3 会随之退出
    	// 然后 main 函数中的 g.Wait() 退出,所有协程都会退出
    	g.Go(func() error {
    		select {
    		case <-ctx.Done():
    			log.Println("errgroup exit...")
    		case <-serverOut:
    			log.Println("server will out...")
    		}
    
    		timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    		// 这里不是必须的,但是如果使用 _ 的话静态扫描工具会报错,加上也无伤大雅
    		defer cancel()
    
    		log.Println("shutting down server...")
    		return server.Shutdown(timeoutCtx)
    	})
    
    	// g3
    	// g3 捕获到 os 退出信号将会退出
    	// g3 退出了所有的协程都能退出么?
    	// g3 退出后, context 将不再阻塞,g2 会随之退出
    	// g2 退出时,调用了 shutdown,g1 会退出
    	// 然后 main 函数中的 g.Wait() 退出,所有协程都会退出
    	g.Go(func() error {
    		quit := make(chan os.Signal, 0)
    		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    
    		select {
    		case <-ctx.Done():
    			return ctx.Err()
    		case sig := <-quit:
    			return fmt.Errorf("get os signal: %v", sig)
    		}
    	})
    
    	fmt.Printf("errgroup exiting: %+v\n", g.Wait())
    }
    

    运行后,使用 ctrl + C 终止程序,终端输出如下

    2021/11/03 10:20:34 errgroup exit...
    2021/11/03 10:20:34 shutting down server...
    errgroup exiting: get os signal: interrupt
    

    这里主要用到了 errgroup 一个出错,其余取消的能力


    四. 总结

    当然除了 Golang 官方提供的扩展库之外,还有很多类似的其他优秀开源工具,例如 bilibili/errgroup,支持设置固定数量的协程数以及失败 cancel 机制和 panic-recover 机制等等,感兴趣的同学可以自行去了解一番。


    五. 参考

    1. https://lailin.xyz/post/go-training-week3-errgroup.html

    2. https://juejin.cn/post/6996300205989560333

    3. https://github.com/golang/sync/tree/master/errgroup

    ♥永远年轻,永远热泪盈眶♥
  • 相关阅读:
    ZOJ 3327 Friend Number
    ZOJ 3324 Machine
    2010-2011 ACM-ICPC, NEERC, Southern Subregional Contest C Explode 'Em All
    UVA 12594 Naming Babies
    POJ 3709 K-Anonymous Sequence
    POJ 1180 Batch Scheduling
    POJ 1160 Post Office
    HDU 3516 Tree Construction
    HDU 3045 Picnic Cows
    UVALive 5097 Cross the Wall
  • 原文地址:https://www.cnblogs.com/failymao/p/15522374.html
Copyright © 2011-2022 走看看