目录

  • 什么是sync.Once
  • 如何应用sync.Once
  • 源码剖析
文章始发于公众号【迈莫coding】https://mp.weixin.qq.com/s/b89PmljELaPaVuLw-YIQKg

什么是sync.Once

Once 能够用来执行且仅仅执行一次动作,经常用于单例对象的初始化场景。

Once 经常用来初始化单例资源,或者并发拜访只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。

sync.Once 只裸露了一个办法 Do,你能够屡次调用 Do 办法,然而只有第一次调用 Do 办法时 f 参数才会执行,这里的 f 是一个无参数无返回值的函数。

如何应用sync.Once

就拿我负责的一个我的项目来说,因为我的项目的配置是挂在第三方平台上,所以在我的项目启动时须要获取资源配置,因为须要一个办法来保障配置仅此只获取一次,因而,咱们思考应用 sync.Once 来获取资源。这样的话,能够避免在其余中央调用获取资源办法,该办法仅此执行一次。

上面我简略写个Demo来演示一个sync.Once如何应用

package mainimport (   "fmt"   "sync")var once sync.Oncevar con stringfunc main() {   once.Do(func() {      con = "hello Test once.Do"   })   fmt.Println(con)}

代码阐明:

代码的话比较简单,就是通过调用Do办法,采纳闭包形式,将字符串("hello Test once.Do")赋值给con,进而打印出值,这就是 sync.Once 的应用,比拟容易上手。

但咱们用一个办法或者框架时,如果不对其一目了然,总有点不太靠谱,感觉心里不虚浮。为此,咱们来聊一聊 sync.Once 的源码实现,让他无处可遁。

源码剖析

接下来剖析 sync.Do 到底是如何实现的,它存储在包sync下 once.go 文件中,源代码如下:

// sync/once.gotype Once struct {   done uint32 // 初始值为0示意还未执行过,1示意曾经执行过   m    Mutex }func (o *Once) Do(f func()) {   // 判断done是否为0,若为0,示意未执行过,调用doSlow()办法初始化   if atomic.LoadUint32(&o.done) == 0 {      // Outlined slow-path to allow inlining of the fast-path.      o.doSlow(f)   }}// 加载资源func (o *Once) doSlow(f func()) {   o.m.Lock()   defer o.m.Unlock()   // 采纳双重检测机制 加锁判断done是否为零   if o.done == 0 {      // 执行完f()函数后,将done值设置为1      defer atomic.StoreUint32(&o.done, 1)      // 执行传入的f()函数      f()   }}

接下来会分为两大部分进行剖析,第一部分为 Once 的构造体组成构造,第二局部为 Do 函数的实现原理,我会在代码上加上正文,保障用心浏览完都有播种。

构造体

type Once struct {   done uint32 // 初始值为0示意还未执行过,1示意曾经执行过   m    Mutex }

首先定义一个struct构造体 Once ,外面存储两个成员变量,别离为 donem

done成员变量

  • 1示意资源未初始化,须要进一步初始化
  • 0示意资源已初始化,无需初始化,间接返回即可

m成员变量

  • 为了避免多个goroutine调用 doSlow() 初始化资源时,造成资源屡次初始化,因而采纳 Mutex 锁机制来保障有且仅初始化一次

Do

func (o *Once) Do(f func()) {   // 判断done是否为0,若为0,示意未执行过,调用doSlow()办法初始化   if atomic.LoadUint32(&o.done) == 0 {      // Outlined slow-path to allow inlining of the fast-path.      o.doSlow(f)   }}// 加载资源func (o *Once) doSlow(f func()) {   o.m.Lock()   defer o.m.Unlock()   // 采纳双重检测机制 加锁判断done是否为零   if o.done == 0 {      // 执行完f()函数后,将done值设置为1      defer atomic.StoreUint32(&o.done, 1)      // 执行传入的f()函数      f()   }

调用 Do 函数时,首先判断done值是否为0,若为1,示意传入的匿名函数 f() 已执行过,无需再次执行;若为0,示意传入的匿名函数 f() 还未执行过,则调用 doSlow() 函数进行初始化。

在 doSlow() 函数中,若并发的goroutine进入该函数中,为了保障仅有一个goroutine执行 f() 匿名函数。为此,须要加互斥锁保障只有一个goroutine进行初始化,同时采纳了双查看的机制(double-checking),再次判断 o.done 是否为 0,如果为 0,则是第一次执行,执行结束后,就将 o.done 设置为 1,而后开释锁。

即便此时有多个 goroutine 同时进入了 doSlow 办法,因为双查看的机制,后续的 goroutine 会看到 o.done 的值为 1,也不会再次执行 f。

这样既保证了并发的 goroutine 会期待 f 实现,而且还不会屡次执行 f。

文章也会继续更新,能够微信搜寻「 迈莫coding 」第一工夫浏览。每天分享优质文章、大厂教训、大厂面经,助力面试,是每个程序员值得关注的平台。