学习sync.Once
目录
sync.Once
基于Go 1.13
sync.Once 是一个只执行一次动作的对象
1 | type Once struct{ |
What is hot path?
hot path 是一系列被非常频繁执行的指令
- 当需要访问struct的第一个字段时,我们可以直接对指针解引用来访问第一个字段。
- 要访问其他字段时,除了结构指针之外, 还需要提供与第一个字段的偏移量
在机器码中,这个偏移量是传递指令的附加值,这会使指令变得更长。对性能的影响是,CPU必须对结构指针添加偏移量以获取想要访问的字段的地址。
因此访问struct的第一个字段的机器码更快,更加紧凑。
这里假设字段在内存中的布局与结构定义中的布局相同,因为编译器可以决定改变内存中结构的字段顺序来优化存储空间,目前go编译器未做这样的优化。
这是一个小优化,只有性能非常重要才值得付出这样的努力。
func (*Once) Do
Go1.13的源码如下,非常的简洁
1 | // Do calls the function f if and only if Do is being called for the |
简单来说,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 | type Once uint32 |
刚才我们提到了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 | var singleton Cache = New(10000, WithTTL(10000*100)) |
Sync.Once实现单例延迟初始化
1 | var ( |