golang源码分析-Once
TOC
Once介绍#
在项目开发中,我们经常会需要调用其他服务或初始化一些资源。此时就需要用到单例模式,通过一个全局变量,在资源首次被使用时服务会去初始化它,其他地方需要使用资源时就用这个全局变量。但是在多线程环境下,这个全局变量可能会被多个线程同时初始化,这时就需要用到sync.Once。
sync.Once能确保对应的变量/资源只被初始化一次,节约内存资源,提高性能。本文就来介绍下它的使用与实现。
基本使用#
终端运行go doc看下sync.Once的文档:
$ go doc sync.Once
package sync // import "sync"
type Once struct {
// Has unexported fields.
}
Once is an object that will perform exactly one action.
A Once must not be copied after first use.
In the terminology of the Go memory model, the return from f “synchronizes
before” the return from any call of once.Do(f).
func (o *Once) Do(f func())
sync.Once对外暴露了一个Do方法,该方法接收一个函数作为参数,这个函数就是业务上仅需要执行一次的逻辑(如上文说的初始化某些资源)。Do方法会确保这个函数只会被执行一次。
func hello() {
fmt.Println("Hello")
}
func main() {
once := sync.Once{}
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
once.Do(hello)
}()
}
wg.Wait()
}
上面的代码中,我们定义了一个hello函数,然后在main函数中,我们创建了一个sync.Once实例once,然后启动了10个协程,每个协程都会调用once.Do(hello)。由于sync.Once的特性,hello函数只会被执行一次,所以最终只会打印一次Hello。关于sync.WaitGroup的使用可以参考我上一篇博客,这里不做介绍。
通过
sync.Once,我们可以确保某个函数只会被执行一次,从而就可以避免重复初始化资源,提高性能。
源码分析#
本文代码基于golang 1.22
sync.Once的定义如下:
type Once struct {
// done indicates whether the action has been performed.
done atomic.Uint32
m Mutex
}
有两个字段,done和m。done是一个atomic.Uint32类型的变量,用来标记是否已经执行过Do方法。m是一个Mutex类型的变量,用来保护done字段。
func (o *Once) Do(f func()) {
if o.done.Load() == 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.Load() == 0 {
defer o.done.Store(1)
f()
}
}
Do方法首先会判断done字段是否为0,如果为0,说明还没有执行过Do方法,那么就调用doSlow方法(通过外联的方式,因为大部分情况下f已经被执行了,不会走到doSlow方法)。doSlow方法中,首先会加锁,接着再判断一下done字段是否为0(双检测机制)。- 执行
f函数,然后将done字段设置为1,表示已经执行过Do方法。
整体实现还是很简单的,通过atomic.Uint32和Mutex来保证Do方法只会被执行一次。