Golang源码分析之golang/sync之singleflight

1.1. 项目介绍

golang/sync库拓展了官方自带的sync库,提供了errgroup、semaphore、singleflight及syncmap四个包,本次分析singlefliht的源代码。
singlefliht用于解决单机协程并发调用下的重复调用问题,常与缓存一起使用,避免缓存击穿。

1.2.使用方法

go get -u golang.org/x/sync

  • 核心API:Do、DoChan、Forget
  • Do:同一时刻对某个Key方法的调用, 只能由一个协程完成,其余协程阻塞直到该协程执行成功后,直接获取其生成的值,以下是一个避免缓存击穿的常见使用方法:
func main() {
   var flight singleflight.Group
   var errGroup errgroup.Group

   // 模拟并发获取数据缓存
   for i := 0; i < 10; i++ {
      i := i
      errGroup.Go(func() error {
         fmt.Printf("协程%v准备获取缓存\n", i)
         v, err, shared := flight.Do("getCache", func() (interface{}, error) {
            // 模拟获取缓存操作
            fmt.Printf("协程%v正在读数据库获取缓存\n", i)
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("协程%v读取数据库生成缓存成功\n", i)
            return "mockCache", nil
         })
         if err != nil {
            fmt.Printf("err = %v", err)
            return err
         }
         fmt.Printf("协程%v获取缓存成功, v = %v, shared = %v\n", i, v, shared)
         return nil
      })
   }
   if err := errGroup.Wait(); err != nil {
      fmt.Printf("errGroup wait err = %v", err)
   }
}
// 输出:只有0号协程实际生成了缓存,其余协程读取生成的结果
协程0准备获取缓存
协程4准备获取缓存
协程3准备获取缓存
协程2准备获取缓存
协程6准备获取缓存
协程5准备获取缓存
协程7准备获取缓存
协程1准备获取缓存
协程8准备获取缓存
协程9准备获取缓存
协程0正在读数据库获取缓存
协程0读取数据库生成缓存成功
协程0获取缓存成功, v = mockCache, shared = true
协程8获取缓存成功, v = mockCache, shared = true
协程2获取缓存成功, v = mockCache, shared = true
协程6获取缓存成功, v = mockCache, shared = true
协程5获取缓存成功, v = mockCache, shared = true
协程7获取缓存成功, v = mockCache, shared = true
协程9获取缓存成功, v = mockCache, shared = true
协程1获取缓存成功, v = mockCache, shared = true
协程4获取缓存成功, v = mockCache, shared = true
协程3获取缓存成功, v = mockCache, shared = true

DoChan:将执行结果返回到通道中,可通过监听通道结果获取方法执行值,这个方法相较于Do来说的区别是执行DoChan后不会阻塞到其中一个协程完成任务,而是异步执行任务,最后需要结果时直接从通道中获取,避免长时间等待。

func testDoChan() {
   var flight singleflight.Group
   var errGroup errgroup.Group
 
   // 模拟并发获取数据缓存
   for i :=; i < 10; i++ {
      i := i
      errGroup.Go(func() error {
         fmt.Printf("协程%v准备获取缓存\n", i)
         ch := flight.DoChan("getCache", func() (interface{}, error) {
            // 模拟获取缓存操作
            fmt.Printf("协程%v正在读数据库获取缓存\n", i)
            time.Sleep( * time.Millisecond)
            fmt.Printf("协程%v读取数据库获取缓存成功\n", i)
            return "mockCache", nil
         })
         res := <-ch
         if res.Err != nil {
            fmt.Printf("err = %v", res.Err)
            return res.Err
         }
         fmt.Printf("协程%v获取缓存成功, v = %v, shared = %v\n", i, res.Val, res.Shared)
         return nil
      })
   }
   if err := errGroup.Wait(); err != nil {
      fmt.Printf("errGroup wait err = %v", err)
   }
}
// 输出结果
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程正在读数据库获取缓存
协程读取数据库获取缓存成功
协程准备获取缓存
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true

2.源码分析

2.1.项目结构

  • singleflight.go:核心实现,提供相关API
  • singleflight_test.go:相关API单元测试

2.2.数据结构

  • singleflight.go
// singleflight.Group
type Group struct {
   mu sync.Mutex       // map的锁
   m  map[string]*call // 保存每个key的调用
}
 
// 一次Do对应的响应结果
type Result struct {
   Val    interface{}
   Err    error
   Shared bool
}
 
// 一个key会对应一个call
type call struct {
   wg sync.WaitGroup
   val interface{} // 保存调用的结果
   err error       // 调用出现的err
   // 该call被调用的次数
   dups  int
   // 每次DoChan时都会追加一个chan在该列表
   chans []chan<- Result
}

2.3.API代码流程

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
   g.mu.Lock()
   if g.m == nil {
      // 第一次执行Do的时候创建map
      g.m = make(map[string]*call)
   }
   // 已经存在该key,对应后续的并发调用
   if c, ok := g.m[key]; ok {
      // 执行次数自增
      c.dups++
      g.mu.Unlock()
      // 等待执行fn的协程完成
      c.wg.Wait()
      // ...
      // 返回执行结果
      return c.val, c.err, true
   }
   
   // 不存在该key,说明第一次调用,初始化一个call
   c := new(call)
   // wg添加,后续其他协程在该wg上阻塞
   c.wg.Add()
   // 保存key和call的关系
   g.m[key] = c
   g.mu.Unlock()
   // 真正执行fn函数
   g.doCall(c, key, fn)
   return c.val, c.err, c.dups >
}
 
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
   normalReturn := false
   recovered := false
 
   // 第三步、最后的设置和清理工作
   defer func() {
      // ...
      g.mu.Lock()
      defer g.mu.Unlock()
      // 执行完成,调用wg.Done,其他协程此时不再阻塞,读到fn执行结果
      c.wg.Done()
      // 二次校验map中key的值是否为当前call,并删除该key
      if g.m[key] == c {
         delete(g.m, key)
      }
      // ...
      // 如果c.chans存在,则遍历并写入执行结果
      for _, ch := range c.chans {
          ch <- Result{c.val, c.err, c.dups >}
        }
      }
   }()
 
   // 第一步、执行fn获取结果
   func() {
      //、如果fn执行过程中panic,将c.err设置为PanicError
      defer func() {
         if !normalReturn {
            if r := recover(); r != nil {
               c.err = newPanicError(r)
            }
         }
      }()
      //、执行fn,获取到执行结果
      c.val, c.err = fn()
      //、设置正常返回结果标识
      normalReturn = true
   }()
 
   // 第二步、fn执行出错,将recovered标识设置为true
   if !normalReturn {
      recovered = true
   }
}

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
   // 一次调用对应一个chan
   ch := make(chan Result,)
   g.mu.Lock()
   if g.m == nil {
      // 第一次调用,初始化map
      g.m = make(map[string]*call)
   }
   // 后续调用,已存在key
   if c, ok := g.m[key]; ok {
      // 调用次数自增
      c.dups++
      // 将chan添加到chans列表
      c.chans = append(c.chans, ch)
      g.mu.Unlock()
      // 直接返回chan,不等待fn执行完成
      return ch
   }
 
   // 第一次调用,初始化call及chans列表
   c := &call{chans: []chan<- Result{ch}}
   // wg加一
   c.wg.Add()
   // 保存key及call的关系
   g.m[key] = c
   g.mu.Unlock()
 
   // 异步执行fn函数
   go g.doCall(c, key, fn)
 
   // 直接返回该chan
   return ch
}

3.总结

  • singleflight经常和缓存获取配合使用,可以缓解缓存击穿问题,避免同一时刻单机大量的并发调用获取数据库构建缓存
  • singleflight的实现很精简,核心流程就是使用map保存每次调用的key与call的映射关系,每个call中通过wg控制只存在一个协程执行fn函数,其他协程等待执行完成后,直接获取执行结果,在执行完成后会删去map中的key
  • singleflight的Do方法会阻塞直到fn执行完成,DoChan方法不会阻塞,而是异步执行fn,并通过通道来实现结果的通知

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/117239.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

要做一名成功的测试,首先得会想?

近在做测试时&#xff0c;突然想到了这么个问题——在测试的过程中对某个功能想得越开&#xff0c;测试就完整&#xff0c;就越彻底&#xff01; 当然我们在产生与该功能相关的想象时&#xff0c;其中最关键的是不能脱离需求&#xff0c;不能脱离该软件本身&#xff1b;不然这…

WebSocket Day02 : 握手连接

前言 握手连接是WebSocket建立通信的第一步&#xff0c;通过客户端和服务器之间的一系列握手操作&#xff0c;确保了双方都支持WebSocket协议&#xff0c;并达成一致的通信参数。握手连接的过程包括客户端发起握手请求、服务器响应握手请求以及双方完成握手连接。完成握手连接后…

优雅的 Dockerfile 是怎样炼成的?

Docker 简介 目前&#xff0c;Docker 主要有两个形态&#xff1a;Docker Desktop 和 Docker Engine。 Docker Desktop 是专门针对个人使用而设计的&#xff0c;支持 Mac&#xff08;已支持arm架构的M系芯片&#xff09; 和 Windows 快速安装&#xff0c;具有直观的图形界面&a…

【教3妹学编程-算法题】117. 填充每个节点的下一个右侧节点指针 II

2哥 : 3妹&#xff0c;听说你昨天去面试了&#xff0c;怎么样啊&#xff1f; 3妹&#xff1a;嗨&#xff0c;别提了&#xff0c;让我回去等通知&#xff0c;估计是没有通知了&#xff0c; 还浪费我请了一天假。 2哥 : 你又请假了啊&#xff0c; 你是怎么跟你那个严厉的老板请假…

微服务架构之路1,服务如何拆分?使用微服务的注意事项?

目录 一、前言二、单体服务的弊端三、微服务化四、服务如何拆分&#xff1f;1、拆分原则2、拆分时机和拆分方法3、拆分实践 五、使用微服务的注意事项1、确保相关业务和利益相关者的支持2、确定微服务的拆分粒度3、遵循微服务架构的原则4、确保接口的稳定性5、关注数据一致性6、…

postMessage

A:端口3000 import React, { useEffect } from react;function App() {useEffect(() > {const childWindow document.getElementById(child).contentWindow;const sendMessageToChild () > {childWindow.postMessage("主页面消息", "http://localhost:…

【QT5之QFtp模块】编译及使用

下载 传送门&#xff1a;https://github.com/qt/qtftp 或者 git clone https://github.com/qt/qtftp.git 下载ZIP&#xff0c;解压待用。 编辑 使用QtCreator打开qtftp.pro; 修改如下&#xff1a; qtftp.pro中&#xff0c;将第21行注释; src/qftp.pro中&#xff0c;将第4行…

Java程序设计2023-第四次上机练习

8-1 三子棋 编写程序&#xff0c;实现简单的三子棋游戏。在三子棋中&#xff0c;双方在33的棋盘中轮流下棋&#xff0c;一方用*示&#xff0c;另一方用O表示。如果一方的3个棋子占据了同一行&#xff0c;同一列或者对角线&#xff0c;则该方获胜。如果棋盘已被棋子占满&#x…

设置DevC++支持c++11标准

1.点击编译选项 2. 设置语言标准 3.点击确认 4.测试代码 使用auto成功 测试&#xff01;

【漏洞复现】Apache_HTTPD_多后缀解析漏洞

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞复现1、基础环境2、漏洞验证 1.3、深度利用GetShell 1.4、修复建议 1.1、漏洞描述 Apache HTTPD 支持一个文件拥有多个后缀&#xff0c;并为不同后缀执…

统计学习方法 条件随机场

文章目录 统计学习方法 条件随机场随机场马尔可夫随机场定义因子分解 条件随机场定义参数化形式简化形式矩阵形式 概率预测问题前向-后向算法概率的计算期望值的计算 学习问题改进的迭代尺度法拟牛顿法 解码问题 统计学习方法 条件随机场 学习李航的《统计学习方法》时&#x…

STM32 IAP应用开发--bootloader升级程序

STM32 IAP应用开发--bootloader升级程序 Chapter1 STM32 IAP应用开发——通过串口/RS485实现固件升级&#xff08;方式2&#xff09;前言什么是IAP&#xff1f;什么是BootLoader&#xff1f; 方案介绍&#xff1a;1&#xff09;bootloader部分&#xff1a;2&#xff09;APP部分…

基于单片机的胎压监测系统的设计

收藏和点赞&#xff0c;您的关注是我创作的动力 文章目录 概要 一、系统整体设计方案二、 系统设计4.1 主流程图 三 系统仿真5.1 系统仿真调试实物 四、 结论 概要 本文以STC89C52单片机为控制核心&#xff0c;通过气压传感器模块对汽车各轮胎的胎压进行实时数据的采集与处理&…

【数据结构】树与二叉树(二):树的表示C语言:树形表示法、嵌套集合表示法、嵌套括号表示法 、凹入表示法

文章目录 5.1 树的基本概念5.1.1 树的定义5.1.2 森林的定义5.1.3 树的术语5.1.4 树的表示1&#xff0e;树形表示法2&#xff0e;嵌套集合表示法结构体创建树主函数 3&#xff0e;嵌套括号表示法结构体创建树嵌套括号表示法主函数 4&#xff0e;凹入表示法结构体创建树凹入表示法…

IDEA 设置代码注释模板

功能简介&#xff1a; 每次看别人代码时&#xff0c;面对毫无注释的类&#xff0c;除了头大还是头大&#xff0c; 以下提供了一种代码类注释模板 新建java类的时候&#xff0c;自动增加类注释&#xff0c;养成代码开发好习惯 效果展示&#xff1a; 代码模板&#xff1a; #if (…

多媒体应用设计师 2023年(含答案回忆版)

以下是小红书上的回忆版 软考考完疯狂回忆&#xff0c;多媒体应用设计师选择题 1.pattern 2.effective 3.merge 4.applications 5.graphic 6.udp 7.rtp 8.rtsp 9.10cm 10.永久 11…97 12.工作技术管理标准 13.管理型元数据 14.premiere 15.wave 16.500km/h 17.3M 18.44000 19.…

Capto2024专为Mac电脑设计的屏幕录制和视频编辑软件

不得不说视频编辑功能&#xff1a;Capto提供了多种视频编辑功能&#xff0c;例如剪辑、旋转、裁剪、调整音频和视频的音量、加入水印、添加注释等&#xff0c;你能够使用Capto编辑你的视频&#xff0c;使之更加专业和生动。有目共睹的是录制完成后&#xff0c;你能够使用Capto提…

深入理解WPF中的依赖注入和控制反转

在WPF开发中&#xff0c;依赖注入&#xff08;Dependency Injection&#xff09;和控制反转&#xff08;Inversion of Control&#xff09;是程序解耦的关键&#xff0c;在当今软件工程中占有举足轻重的地位&#xff0c;两者之间有着密不可分的联系。今天就以一个简单的小例子&…

Paddle炼丹炉炸了Unexpected BUS error encountered in DataLoader worker

Paddle训练报错&#xff0c;内存不足 python train.py -c config/ResNet_W18.yaml修改配置文件config/ResNet_W18.yaml # 原配置 loader:num_workers: 4use_shared_memory: True# 修改后 loader:num_workers: 2use_shared_memory: False

数据分析实战 | 关联规则分析——购物车分析

目录 一、数据及分析对象 二、目的及分析任务 三、方法及工具 四、数据读入 五、数据理解 六、数据预处理 七、生成频繁项集 八、计算关联度 九、可视化 一、数据及分析对象 数据集链接&#xff1a;Online Retail.xlsx 该数据集记录了2010年12月01日至2011年12月09日…