Once介绍
在项目开发中,我们经常会需要调用其他服务或初始化一些资源。此时就需要用到单例模式,通过一个全局变量,在资源首次被使用时服务会去初始化它,其他地方需要使用资源时就用这个全局变量。但是在多线程环境下,这个全局变量可能会被多个线程同时初始化,这时就需要用到sync.Once
。
sync.Once
能确保对应的变量/资源只被初始化一次,节约内存资源,提高性能。本文就来介绍下它的使用与实现。
基本使用
终端运行go doc
看下sync.Once
的文档:
|
|
sync.Once
对外暴露了一个Do
方法,该方法接收一个函数作为参数,这个函数就是业务上仅需要执行一次的逻辑(如上文说的初始化某些资源)。Do
方法会确保这个函数只会被执行一次。
|
|
上面的代码中,我们定义了一个hello
函数,然后在main
函数中,我们创建了一个sync.Once
实例once
,然后启动了10个协程,每个协程都会调用once.Do(hello)
。由于sync.Once
的特性,hello
函数只会被执行一次,所以最终只会打印一次Hello
。关于sync.WaitGroup
的使用可以参考我上一篇博客,这里不做介绍。
通过
sync.Once
,我们可以确保某个函数只会被执行一次,从而就可以避免重复初始化资源,提高性能。
源码分析
本文代码基于golang 1.22
sync.Once
的定义如下:
|
|
有两个字段,done
和m
。done
是一个atomic.Uint32
类型的变量,用来标记是否已经执行过Do
方法。m
是一个Mutex
类型的变量,用来保护done
字段。
|
|
Do
方法首先会判断done
字段是否为0,如果为0,说明还没有执行过Do
方法,那么就调用doSlow
方法(通过外联的方式,因为大部分情况下f已经被执行了,不会走到doSlow
方法)。doSlow
方法中,首先会加锁,接着再判断一下done
字段是否为0(双检测机制)。- 执行
f
函数,然后将done
字段设置为1,表示已经执行过Do
方法。
整体实现还是很简单的,通过atomic.Uint32
和Mutex
来保证Do
方法只会被执行一次。