目录
- 什么是 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 main
import (
"fmt"
"sync"
)
var once sync.Once
var con string
func 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.go
type 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,外面存储两个成员变量,别离为 done 和 m。
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」第一工夫浏览。每天分享优质文章、大厂教训、大厂面经,助力面试,是每个程序员值得关注的平台。