

学习sync.Once
source link: https://sikasjc.github.io/2020/01/18/syncOnce/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

sync.Once
基于Go 1.13
sync.Once 是一个只执行一次动作的对象
1
2
3
4
5
6
7
8
9
type Once struct{
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/x86),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
What is hot path?
hot path 是一系列被非常频繁执行的指令
- 当需要访问struct的第一个字段时,我们可以直接对指针解引用来访问第一个字段。
- 要访问其他字段时,除了结构指针之外, 还需要提供与第一个字段的偏移量
在机器码中,这个偏移量是传递指令的附加值,这会使指令变得更长。对性能的影响是,CPU必须对结构指针添加偏移量以获取想要访问的字段的地址。
因此访问struct的第一个字段的机器码更快,更加紧凑。
这里假设字段在内存中的布局与结构定义中的布局相同,因为编译器可以决定改变内存中结构的字段顺序来优化存储空间,目前go编译器未做这样的优化。
这是一个小优化,只有性能非常重要才值得付出这样的努力。
func (*Once) Do
Go1.13的源码如下,非常的简洁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.
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()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
简单来说,once.Do 中的函数只会执行一次,并保证 once.Do 返回时,传入Do的函数已经执行完成。(多个 goroutine 同时执行 once.Do 的时候,可以保证抢占到 once.Do 执行权的 goroutine 执行完 once.Do 后,其他 goroutine 才能得到返回 )
从代码流程中,我们可以很清楚的看到,Once.Do()的逻辑:
- 首先考虑到为了并发安全加mutex,但是Once.Do()对性能有一定要求,所以选用原子操作,LoadUint32获取锁更快
- 判断执行标志,若已执行过就直接返回
- 因为是判断执行标志而不修改,就会有多个goroutine同时判断为true,之后用mutex调用函数f()
- 获得mutex后,先检查执行标志,以免重复执行
- 接着调用f()
- 然后将执行标志done置为1
- 最后解开mutex,当其他落入doSlow的goroutine在重复上述过程时就可以保证f()只被调用一次。
一种错误实现
注意在源码中,提到了一种错误实现
1
2
3
4
5
6
type Once uint32
func (o *Once) Do(f func()) {
if atomic.CompareAndSwapUint32((*uint32)(o), 0, 1) {
f()
}
}
刚才我们提到了Once.Do()
可以保证了只有当f()
执行完毕时,Once.Do()
才会返回。这意味着如果多个goroutines同时调用Once.Do()
,那么f()
当然会执行一次,但是所有的调用都会等到f()
完成(它们会被阻塞)。
当我们使用sync.Once
时,依赖于此行为,依赖于在调用Once.Do()
完成f()
之后的情况。这样我们才可以安全地使用f()
初始化变量等操作,而不会出现竞态条件。
但这种实现,并没有这样的保证,抢占到执行权的goroutine会执行f()
,而其他goroutine会直接返回,这时f()
并没有执行完。
init()和sync.Once
- Go语言规范保证了包的init()函数只能被调用一次,并且只能从单个线程调用所有函数(并不是说
init()
不能启动goroutine,但是除非使init()
成为多线程,否则它们是线程安全的) - 使用
sync.Once.Do()
的原因是,控制是否以及何时执行某些代码。init()
函数将在应用程序启动时调用。sync.Once.Do()
允许执行延迟初始化之类的操作,例如在第一次请求资源时创建资源,而不是在应用程序启动时创建资源; 或仅在实际需要时才初始化资。
简而言之,init()
在包导入时执行,而sync.Once.Do()
在你的控制之下; 你可以决定是否调用,何时调用以及它的作用。
程序启动时初始化
1
2
3
4
5
var singleton Cache = New(10000, WithTTL(10000*100))
func Singleton() Cache {
return singleton
}
Sync.Once实现单例延迟初始化
1
2
3
4
5
6
7
8
9
10
11
var (
singleton Cache
once sync.Once
)
func Singleton() Cache(){
once.Do(func(){
singleton = New(10000, WithTTL(10000*100))
})
return singleton
}
Reference
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK