八. 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 的作用类似,但是它提供了更加丰富的功能以及更低的使用成本:
- 和context集成;
- 能够对外传播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 机制等等,感兴趣的同学可以自行去了解一番。
五. 参考
八. Go并发编程--errGroup的更多相关文章
- Python之路(第三十八篇) 并发编程:进程同步锁/互斥锁、信号量、事件、队列、生产者消费者模型
一.进程锁(同步锁/互斥锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理. 例 ...
- Python并发编程之深入理解yield from语法(八)
大家好,并发编程 进入第八篇. 直到上一篇,我们终于迎来了Python并发编程中,最高级.最重要.当然也是最难的知识点--协程. 当你看到这一篇的时候,请确保你对生成器的知识,有一定的了解.当然不了解 ...
- java并发编程笔记(八)——死锁
java并发编程笔记(八)--死锁 死锁发生的必要条件 互斥条件 进程对分配到的资源进行排他性的使用,即在一段时间内只能由一个进程使用,如果有其他进程在请求,只能等待. 请求和保持条件 进程已经保持了 ...
- java并发编程工具类JUC第八篇:ConcurrentHashMap
在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...
- 并发编程从零开始(八)-ConcurrentHashMap
并发编程从零开始(八)-ConcurrentHashMap 5.5 ConcurrentHashMap HashMap通常的实现方式是"数组+链表",这种方式被称为"拉链 ...
- 十二. Go并发编程--sync/errGroup
一.序 这一篇算是并发编程的一个补充,起因是当前有个项目,大概の 需求是,根据kafka的分区(partition)数,创建同等数量的 消费者( goroutine)从不同的分区中消费者消费数据,但是 ...
- 转: 【Java并发编程】之十八:第五篇中volatile意外问题的正确分析解答(含代码)
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17382679 在<Java并发编程学习笔记之五:volatile变量修饰符-意料之外 ...
- Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)
前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 packa ...
- java并发编程实战《八》管程
管程:并发编程的万能钥匙 为什么 Java 在 1.5 之前仅仅提供了 synchronized 关键字及 wait().notify().notifyAll() 这三个看似从天而降的方法? Java ...
随机推荐
- Win7 ASP连接数据库“未找到提供程序.该程序可能未正确安装”问题
是自己装的64位Windows 7系统的原因,默认64位环境下,IIS应用程序池未启用32位应用程序,我们只需要启用一下就可以了.打开IIS 7,定位到"应用程序池",然后选择使用 ...
- SpringBoot 整合 Elastic Stack 最新版本(7.14.1)分布式日志解决方案,开源微服务全栈项目【有来商城】的日志落地实践
一. 前言 日志对于一个程序的重要程度不用过多的言语修饰,本篇将以实战的方式讲述开源微服务全栈项目 有来商城 是如何整合当下主流日志解决方案 ELK +Filebeat . 话不多说,先看实现的效果图 ...
- Oracle基本入门
一.数据的存储 1.java 程序中的对象:数组.集合保存.当运行的程序结束的时候,里面的数据就消亡. 2.文件存储系统: 存在的缺陷: 2.1)没有明确的数据类型划分. 2.2)没有用户身份验证机制 ...
- sublime text 3 在Windows下配置sublimelinter-php的路径问题
首先用package control安装sublimelinter和sublimelinter-php,然后依次点击菜单preference-package settings-sublimelinte ...
- 在反序列化数据的时候报错raise JSONDecodeError("Expecting value", s, err.value) from None json.decode
今天在爬取某网站数据内容适合,通过正则匹配拿到了需要的内容字符串,但是在反序列化的时候竟然报错,大概意思知道他不是json的期望值,那么我就会像是不是数据内有一些内容是由于编码的问题导致的呢?因为之前 ...
- ❤️Python接口自动化,一文告诉你连接各大【数据库】建议收藏❤️
@ 目录 前言 常见数据库 Mysql Oracle sql-server PostgreSQL MongoDB Redis 前言 相信很多小伙伴在使用python进行自动化测试的时候,都会涉及到数据 ...
- BATJ经典软件测试面试题我连答案都告诉你了,如果你不想月薪上万,那你别看!
单项选择题:共20小题,每小题1 分,满分20分:请将答案填入题后括号中. 1. 在软件生命周期的哪一个阶段,软件缺陷修复费用最低(A) A.需求分析(编制产品说明书) B.设计 C.编码 ...
- 《集体智慧编程学习笔记》——Chapter2:提供推荐
知识点: 1. 协作型过滤--Collaboraive Filtering 通常的做法是对一群人进行搜索,并从中找出与我们品味相近的一小群人,算法会对这些人的偏好进行考察,并将它们组合起来构造出一个经 ...
- farOs 介绍
nGame nGame 一款文字游戏服务端框架;用于快速构建:自由探索.武侠.修真.模拟,回合制,剧本杀.动态语言小说.等服务器 如果你有期望实现的功能请加Q群 ngame计划 完善框架功能 farO ...
- 跳表--怎么让一个有序链表能够进行"二分"查找?
对于一个有序数组,如果要查找其中的一个数,我们可以使用二分查找(Binary Search)算法,将它的时间复杂度降低为O(logn).那查找一个有序链表,有没有办法将其时间复杂度也降低为O(logn ...