详解Go sync.Once

什么是sync.Once

在很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只初始化一次service等。Go语言中的sync包中提供了一个针对只执行一次场景的解决方案:sync.Once

基本使用

假设需要初始化blogController,先声明once:

var blogControllerOnce sync.Once

用once初始化blogController:



func NewBlogController(blogService service.BlogService) *BlogControllerImpl {

   blogControllerOnce.Do(func() {

      blogController = &BlogControllerImpl{

         BlogService:     blogService,

      }

   })

   return blogController

}

这样能保证blogController的初始化过程只被执行一次

源码分析

Once结构体定义如下

type Once struct {

   done uint32

   m    Mutex

}
  • done是标志位,用来判断方法f是否被执行过,初始值为0,当方法f第一次执行完毕时,done被设为1
  • m用来控制不同goroutine同时执行Done时,保证只有一个goroutine能执行

once.Do:

func (o *Once) Do(f func()) {

 if atomic.LoadUint32(&o.done) == 0 {

 o.doSlow(f)

   }

}

这里用atomic.LoadUint32获取done的值,如果为0,说明还没有初始化过,调用doSlow执行参数中的方法,否则直接退出

先看看该原子读操作:

LoadUint32方法把一个32位整数读出来,保证其原子性

为什么会有这样的需求?难道读写一个整数不是CPU指令级别能保证的原子操作吗?目前通用的CPU指令能保证原子地读取32/64位整数,该原子操作好像没有必要

但这里是为了兼容不同的计算机体系结构,从语言层面提供一个统一的函数。可能有些CPU不支持原子读取32/64位整数,例如只保存一次读取一个字节的数据,这时语言层面的原子操作就很有必要

同时原子操作还保证原子写后会立即将数据刷新到内存,原子读从内存中读

现在编译器可能会做优化,在代码运气期间对变量的读写直接访问寄存器,而不写入内存,以提高性能。这在同一个执行上下文是没有问题的,而如果需要跨线程访问,则永远看不到数据的变化

举个例子:

var x int64 = 0



func storeFunc() {

   for i := 0; ; i++ {

      if i%2 == 0 {

         x = 1

      } else {

         x = 2

      }

      // time.Sleep(10 * time.Millisecond)

 }

}



func main() {

   go storeFunc()



   for {

      time.Sleep(1 * time.Second)

      fmt.Printf("%x\n", x)

   }

}
  1. 定义全局遍历x,初始化为0
  2. 新起goroutine,在storeFunc中不断修改x的值为1或2
  3. 主goroutine中每隔一秒检查x的值

实验结果为,每次x的值都为0。这里因为在storeFunc修改x太频繁,编译器做了优化,直接在寄存器上操作,而每把结果刷到内存中,若果每次for循环暂停一会,就可以在主goroutine中看到修改的值

因此这里用原子读写操作,保证了可见性原子性

回到once.Do,如果没有执行过,就执行doSlow方法:

func (o *Once) doSlow(f func()) {

   o.m.Lock()

   defer o.m.Unlock()

   if o.done == 0 {

      defer atomic.StoreUint32(&o.done, 1)

      f()

   }

}

这是标准的双重检测实现单例模式,加锁后如果o.Done为0(因为上一步加了锁,单协程情况下本身有原子性,因此不用原子读取),说明还执行过,执行f()

doSlow中的o.done == 0这个判读是必须的,因为可能会出现A,B俩个协程都进行了LoadUint32判断,并且都是true,如果不进行第二次校验的话,f就会被调用两次,和预期不符

有一个思考点,Do方法中的atomic.LoadUint32(&o.done),能够替换成以下代码吗?

if atomic.CompareAndSwapUint32(&o.done, 0, 1) {

    f()

}

答案是不可以,因为如果先将done设为1,再执行f,如果同时有另一个goroutine进来,发现已经执行完毕了,就使用f执行后的结果,但此时第一个goroutine还没开始执行,会造成程序错误

总结

本文主要对sync.Once进行了源码分析,知道其工作原理,并介绍了原子操作

参考文档

  1. 原子变量:https://www.jianshu.com/p/2c7797df9b2b
  2. sync.once详解:https://blog.csdn.net/qq_36310758/article/details/115326176
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]和\[2\]提供了关于sync.Once的使用示例。sync.Once是Go语言中的一个同步原语,用于确保某个操作只执行一次。在示例中,sync.Once被用来保证onceBody函数只被执行一次。通过调用once.Do(onceBody),可以确保onceBody函数只会在第一次调用时执行,后续的调用都会被忽略。这在并发编程中非常有用,可以避免重复执行某个操作。 引用\[3\]提供了一个更具体的示例,展示了如何使用sync.Once来获取一个客户端对象。在这个示例中,getMyClient函数使用sync.Once来确保只有在第一次调用时才会创建一个新的MyClient对象,后续的调用都会返回同一个对象。 根据提供的代码片段,var UserSrvOnce sync.Once是一个sync.Once类型的变量声明。根据sync.Once的用途,可以推测UserSrvOnce可能是用来确保某个操作只执行一次的。具体的操作需要根据代码的上下文来确定。 #### 引用[.reference_title] - *1* [Golang sync.Once详解](https://blog.csdn.net/neweastsun/article/details/127562284)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Golang sync.Once 简介与用法](https://blog.csdn.net/K346K346/article/details/87622326)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值