首页 > 其他分享 >Go sync.Once:简约而不简单的并发利器

Go sync.Once:简约而不简单的并发利器

时间:2023-04-12 22:09:01浏览次数:42  
标签:初始化 Do sync done func Go Once

原创文章,如需转载请联系 作者:陈明勇 公众号:Go技术干货 qrcode_for_gh_6dd2704fa679_258.jpg

简介

在某些场景下,我们需要初始化一些资源,例如单例对象、配置等。实现资源的初始化有多种方法,如定义 package 级别的变量、在 init 函数中进行初始化,或者在 main 函数中进行初始化。这三种方式都能确保并发安全,并在程序启动时完成资源的初始化。

然而,有时我们希望采用延迟初始化的方式,在我们真正需要资源的时候才进行初始化,这种需要确保并发安全,在这种情况下,Go 语言中的 sync.Once 提供一个优雅且并发安全的解决方案,本文将对其进行介绍。

sync.Once 基本概念

什么是 sync.Once

sync.OnceGo 语言中的一种同步原语,用于确保某个操作或函数在并发环境下只被执行一次。它只有一个导出的方法,即 Do,该方法接收一个函数参数。在 Do 方法被调用后,该函数将被执行,而且只会执行一次,即使在多个协程同时调用的情况下也是如此。

sync.Once 的应用场景

sync.Once 主要用于以下场景:

  • 单例模式:确保全局只有一个实例对象,避免重复创建资源。
  • 延迟初始化:在程序运行过程中需要用到某个资源时,通过 sync.Once 动态地初始化该资源。
  • 只执行一次的操作:例如只需要执行一次的配置加载、数据清理等操作。

sync.Once 应用实例

单例模式

在单例模式中,我们需要确保一个结构体只被初始化一次。使用 sync.Once 可以轻松实现这一目标。

package main

import (
   "fmt"
   "sync"
)

type Singleton struct{}

var (
   instance *Singleton
   once     sync.Once
)

func GetInstance() *Singleton {
   once.Do(func() {
      instance = &Singleton{}
   })
   return instance
}

func main() {
   var wg sync.WaitGroup

   for i := 0; i < 5; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         s := GetInstance()
         fmt.Printf("Singleton instance address: %p\n", s)
      }()
   }

   wg.Wait()
}

上述代码中,GetInstance 函数通过 once.Do() 确保 instance 只会被初始化一次。在并发环境下,多个协程同时调用 GetInstance 时,只有一个协程会执行 instance = &Singleton{},所有协程得到的实例 s 都是同一个。

延迟初始化

有时候希望在需要时才初始化某些资源。使用 sync.Once 可以实现这一目标。

package main

import (
   "fmt"
   "sync"
)

type Config struct {
   config map[string]string
}

var (
   config *Config
   once   sync.Once
)

func GetConfig() *Config {
   once.Do(func() {
      fmt.Println("init config...")
      config = &Config{
         config: map[string]string{
            "c1": "v1",
            "c2": "v2",
         },
      }
   })
   return config
}

func main() {
   // 第一次需要获取配置信息,初始化 config
   cfg := GetConfig()
   fmt.Println("c1: ", cfg.config["c1"])

   // 第二次需要,此时 config 已经被初始化过,无需再次初始化
   cfg2 := GetConfig()
   fmt.Println("c2: ", cfg2.config["c2"])
}

在这个示例中,定义了一个 Config 结构体,它包含一些设置信息。使用 sync.Once 来实现 GetConfig 函数,该函数在第一次调用时初始化 Config。这样,我们可以在真正需要时才初始化 Config,从而避免不必要的开销。

sync.Once 实现原理

type Once struct {
   // 表示是否执行了操作
   done uint32
   // 互斥锁,确保多个协程访问时,只能一个协程执行操作
   m    Mutex
}

func (o *Once) Do(f func()) {
   // 判断 done 的值,如果是 0,说明 f 还没有被执行过
   if atomic.LoadUint32(&o.done) == 0 {
      // 构建慢路径(slow-path),以允许对 Do 方法的快路径(fast-path)进行内联
      o.doSlow(f)
   }
}

func (o *Once) doSlow(f func()) {
   // 加锁
   o.m.Lock()
   defer o.m.Unlock()
   // 双重检查,避免 f 已被执行过
   if o.done == 0 {
      // 修改 done 的值
      defer atomic.StoreUint32(&o.done, 1)
      // 执行函数
      f()
   }
}

sync.Once 结构体包含两个字段:donemudone 是一个 uint32 类型的变量,用于表示操作是否已经执行过;m 是一个互斥锁,用于确保在多个协程访问时,只有一个协程能执行操作。

sync.Once 结构体包含两个方法:DodoSlowDo 方法是其核心方法,它接收一个函数参数 f。首先它会通过原子操作atomic.LoadUint32(保证并发安全) 检查 done 的值,如果为 0,表示 f 函数没有被执行过,然后执行 doSlow 方法。

doSlow 方法里,首先对互斥锁 m 进行加锁,确保在多个协程访问时,只有一个协程能执行 f 函数。接着再次检查 done 变量的值,如果 done 的值仍为 0,说明 f 函数没有被执行过,此时执行 f 函数,最后通过原子操作 atomic.StoreUint32done 变量的值设置为 1。

为什么会封装一个 doSlow 方法

doSlow 方法的存在主要是为了性能优化。将慢路径(slow-path)代码从 Do 方法中分离出来,使得 Do 方法的快路径(fast-path)能够被内联(inlined),从而提高性能。

为什么会有双重检查(double check)的写法

从源码可知,存在两次对 done 的值的判断。

  • 第一次检查:在获取锁之前,先使用原子加载操作 atomic.LoadUint32 检查 done 变量的值,如果 done 的值为 1,表示操作已执行,此时直接返回,不再执行 doSlow 方法。这一检查可以避免不必要的锁竞争。
  • 第二次检查:获取锁之后,再次检查 done 变量的值,这一检查是为了确保在当前协程获取锁期间,其他协程没有执行过 f 函数。如果 done 的值仍为 0,表示 f 函数没有被执行过。

通过双重检查,可以在大多数情况下避免锁竞争,提高性能。

加强的 sync.Once

sync.Once 提供的 Do 方法并没有返回值,意味着如果我们传入的函数如果发生 error 导致初始化失败,后续调用 Do 方法也不会再初始化。为了避免这个问题,我们可以实现一个 类似 sync.Once 的并发原语。

package main

import (
   "sync"
   "sync/atomic"
)


type Once struct {
   done uint32
   m    sync.Mutex
}

func (o *Once) Do(f func() error) error {
   if atomic.LoadUint32(&o.done) == 0 {
      return o.doSlow(f)
   }
   return nil
}

func (o *Once) doSlow(f func() error) error {
   o.m.Lock()
   defer o.m.Unlock()
   var err error
   if o.done == 0 {
      err = f()
      // 只有没有 error 的时候,才修改 done 的值
      if err == nil {
         atomic.StoreUint32(&o.done, 1)
      }
   }
   return err
}

上述代码实现了一个加强的 Once 结构体。与标准的 sync.Once 不同,这个实现允许 Do 方法的函数参数返回一个 error。如果执行函数没有返回 error,则修改 done 的值以表示函数已执行。这样,在后续的调用中,只有在没有发生 error 的情况下,才会跳过函数执行,避免初始化失败。

sync.Once 的注意事项

死锁

通过分析 sync.Once 的源码,可以看到它包含一个名为 m 的互斥锁字段。当我们在 Do 方法内部重复调用 Do 方法时,将会多次尝试获取相同的锁。但是 mutex 互斥锁并不支持可重入操作,因此这将导致死锁现象。

func main() {
   once := sync.Once{}
   once.Do(func() {
      once.Do(func() {
         fmt.Println("init...")
      })
   })
}

初始化失败

这里的初始化失败指的是在调用 Do 方法之后,执行 f 函数的过程中发生 error,导致执行失败,现有的 sync.Once 设计我们是无法感知到初始化的失败的,为了解决这个问题,我们可以实现一个类似 sync.Once 的加强 once,前面的内容已经提供了具体实现。

小结

本文详细介绍了 Go 语言中的 sync.Once,包括它的基本定义、使用场景和应用实例以及源码分析等。在实际开发中,sync.Once 经常被用于实现单例模式和延迟初始化操作。

虽然 sync.Once 简单而又高效,但是错误的使用可能会造成一些意外情况,需要格外小心。

总之,sync.OnceGo 中非常实用的一个并发原语,可以帮助开发者实现各种并发场景下的安全操作。如果遇到只需要初始化一次的场景,sync.Once 是一个非常好的选择。

标签:初始化,Do,sync,done,func,Go,Once
From: https://blog.51cto.com/chenmingyong/6186297

相关文章

  • Go微服务框架go-kratos实战学习08:负载均衡基本使用
    微服务框架go-kratos中负载均衡使用一、介绍在前面这篇文章负载均衡和它的算法介绍,讲了什么是负载均衡以及作用、算法介绍。go-kratos的负载均衡主要接口是Selector,它是一个可插拔的设计。因为它设计的都是接口,只要实现了接口就实现了负载均衡。go-kratos在目录下提供了......
  • Go语言入门6(struct 结构体)
    结构体​ 结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员结构体声明type+结构体名+struct+{成员列表}​ ⭐如果结构体成员名字是以大写字母开头的,那么该成员就是导出的。这是Go语言导出规则决定的。一个结构体可能同时包......
  • Django自带的Admin后台中如何获取当前登录用户
    需求背景在使用Django快速开发一个IT电脑、显示器资产管理小系统的时候,遇到一个问题是,当变更资产设备(新增、修改、删除)的时候,能记录是谁在什么时间进行的变更。确认的是肯定是登录状态,但是在使用Django的signal中获取不到当前登录的用户问题演示1、定义资产设备模型和 自定义日志......
  • 快速搭建一个go语言web后端服务脚手架
    快速搭建一个go语言web后端服务脚手架源码:https://github.com/weloe/go-web-demoweb框架使用gin,数据操作使用gorm,访问控制使用casbin首先添加一下自定义的middlewarerecover_control.go,统一处理panicerror返回的信息packagemiddlewareimport( "fmt" "github.com/gin-......
  • UVa 11498 Division of Nlogonia (water ver.)
    11498-DivisionofNlogoniaTimelimit:1.000secondshttp://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=24&page=show_problem&problem=2493TheProblemAftercenturiesofhostilitiesandskirmishesbetweenthefour......
  • UVa 757 / POJ 1042 / East Central North America 1999 Gone Fishing (枚举&贪心&想
    757-GoneFishingTimelimit:3.000secondshttp://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=698http://poj.org/problem?id=1042Johnisgoingonafishingtrip.Hehas h hoursavailable( ),andther......
  • go语言学习-gin框架中间件
    全局中间件所有的请求都经过此中间件//所有请求经过此中间件packagemainimport( "fmt" "time" "github.com/gin-gonic/gin")//定义中间件funcMiddleWare()gin.HandlerFunc{ returnfunc(ctx*gin.Context){ t:=time.Now() fmt.Println("中间件开始执行了......
  • 在django中自动删除超过10天的数据
    需求:比如过期10天的产品自动删除记录posting_date=models.DateTimeField(auto_now_add=True)#purge_old_data.pyfromdjango.core.management.baseimportBaseCommand,CommandErrorfromcus_leads.modelsimportCustomerLeadsfromdatetimeimportdatetime,timedel......
  • MongoDB复制集环境滚动构建索引
    滚动构建索引每次最多取出一个复制集成员,从辅助成员开始,并在该成员上以standalone方构建索引。滚动索引构建至少需要一次副本集选举。 考虑点1.唯一性索引使用滚动方式构建唯一性索引,你必须停止集合上所有写操作。否则,只能在主节点上使用db.collection.createIndex()创建。2......
  • djangoadmin 过期显示红色字体
    参考:https://docs.djangoproject.com/en/1.8/topics/i18n/timezones/#naive-and-aware-datetime-objects起因:正常的dateime.now()得到的日期不能和Django数据库里面存储的日期数据做对比,两个解决办法:1、是把Django配置里面的USE_TZ设置成False,这样Django的数据就没有时区信息了......