sync.Once 的使用场景
sync.Once 是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同。
- init 函数是当所在的 package 首次被加载时执行,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间。
- sync.Once 可以在代码的任意位置初始化和调用,因此可以延迟到使用时再执行,并发场景下是线程安全的。
使用方法
在多数情况下,sync.Once 被用于控制变量的初始化,这个变量的读写满足如下三个条件:
- 当且仅当第一次访问某个变量时,进行初始化(写);
- 变量初始化过程中,所有读都被阻塞,直到初始化完成;
- 变量仅初始化一次,初始化完成后驻留在内存里。
sync.Once 仅提供了一个方法 Do,参数 f 是对象初始化函数。
func (o *Once) Do(f func())
- 只有在当前的 Once 实例第一次调用 Do 方法时,才会真正执行 f。哪怕在多次调用 Do 中间 f 的值有所变化,也只会被实际调用一次;
- Do 针对的是只希望执行一次的初始化操作,由于f 是没有参数的,如果需要传参,可以采用包装一层 func 的形式来实现:config.once.Do(func() { config.init(filename) })
在对f 的调用返回之前,不会返回对Do的调用,所以如果f方法中又调用来Do方法,将会死锁。所以不要做这样的操作:
func main() {var once sync.Onceonce.Do(func() {once.Do(func() {fmt.Println("hello kenmawr.")})})}
如果 f 抛出了 panic,此时Do会认为f已经返回,后续多次调用Do也不会再触发对 f 的调用。
作者:ag9920
链接:https://juejin.cn/post/7088305487753510925
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
使用示例
在实际的工作中,你可能会有这样的需求:让代码只执行一次,哪怕是在高并发的情况下,比如创建一个单例。
针对这种情形,Go 语言为我们提供了 sync.Once 来保证代码只执行一次,如下所示:
package mainimport ("fmt""sync")func main() {var once sync.Oncedone := make(chan bool)for i := 0; i < 10; i++ {go func() {//把要执行的函数(方法)作为参数传给once.Do方法即可once.Do(greet)done <- true}()}for i := 0; i < 10; i++ {<-done}}func greet() {fmt.Println("hello world")}
输出结果:
hello world
虽然启动了 10 个协程来执行 greet 函数,但是因为用了 once.Do 方法,所以函数 greet 只会被执行一次。也就是说在高并发的情况下,sync.Once 也会保证 greet 函数只执行一次。
问题
sync.Once类型值的Do方法是怎么保证只执行参数函数一次的?
与 sync.WaitGroup 类型一样,sync.Once 类型(以下简称 Once 类型)也属于结构体类型,同样也是开箱即用和并发安全的。
由于这个类型中包含了一个 sync.Mutex 类型的字段,所以,复制该类型的值也会导致功能的失效。
Once 类型的 Do 方法只接受一个参数,这个参数的类型必须是 func() ,即:无参数声明和结果声明的函数。
该方法的功能并不是对每一种参数函数都只执行一次,而是只执行“首次被调用时传入的”那个函数,并且之后不会再执行任何参数函数。
func main() {var count intincrement := func() { count++ }decrement := func() {fmt.Println("enter decrement")count--}var once sync.Onceonce.Do(increment)once.Do(decrement)fmt.Printf("count is %v", count) // count is 1}
可以看到 count 并不是 0,同时呢我们发现 decrement 函数中并没有进去,所以就验证了上面的说法。once 只执行一次 Do 方法。
如果你有多个只需要执行一次的函数,那么就应该为它们中的每一个都分配一个 sync.Once 类型的值(以下简称 Once 值)
func main() {var count intincrement := func() { count++ }decrement := func() {fmt.Println("enter decrement")count--}var onceIn sync.Once // 给 increment 函数声明一个 Once 类型值var onceDe sync.Once // 给 decrement 函数声明一个 Once 类型值onceIn.Do(increment)onceDe.Do(decrement)fmt.Printf("count is %v", count)// enter decrement// count is 0}
实战用法
任何只希望执行一次的操作
初始化
很多同学可能会有疑问,我直接在 init() 函数里面做初始化不就可以了吗?效果上是一样的,为什么还要用 sync.Once,这样还需要多声明一个 once 对象。
原因在于:
- init() 函数是在所在包首次被加载时执行,若未实际使用,既浪费了内存,又延缓了程序启动时间。
- sync.Once 可以在任何位置调用,而且是并发安全的,我们可以在实际依赖某个变量时才去初始化,这样「延迟初始化」从功能上讲并无差异,但可以有效地减少不必要的性能浪费。
我们来看 Golang 官方的 html 库中的一个例子,我们经常使用的转义字符串函数
func UnescapeString(s string) string
在进入函数的时候,首先就会依赖包里内置的 populateMapsOnce 实例(本质是一个 sync.Once) 来执行初始化 entity 的操作。这里的entity是一个包含上千键值对的 map,如果init()时就去初始化,会浪费内存。
var populateMapsOnce sync.Oncevar entity map[string]runefunc populateMaps() {entity = map[string]rune{"AElig;": '\U000000C6',"AMP;": '\U00000026',"Aacute;": '\U000000C1',"Abreve;": '\U00000102',"Acirc;": '\U000000C2',// 省略后续键值对}}func UnescapeString(s string) string {populateMapsOnce.Do(populateMaps)i := strings.IndexByte(s, '&')if i < 0 {return s}// 省略后续的实现...}
单例模式
开发中我们经常会实现 Getter 来暴露某个非导出的变量,这个时候就可以把 once.Do 放到 Getter 里面,完成单例的创建。
package mainimport ("fmt""sync")type Singleton struct{}var singleton *Singletonvar once sync.Oncefunc GetSingletonObj() *Singleton {once.Do(func() {fmt.Println("Create Obj")singleton = new(Singleton)})return singleton}func main() {var wg sync.WaitGroupfor i := 0; i < 5; i++ {wg.Add(1)go func() {defer wg.Done()obj := GetSingletonObj()fmt.Printf("%p\n", obj)}()}wg.Wait()}/*--------- 输出 -----------Create Obj0x119f4280x119f4280x119f4280x119f4280x119f428**/
关闭channel
一个channel如果已经被关闭,再去关闭的话会 panic,此时就可以应用 sync.Once 来帮忙。
type T inttype MyChannel struct {c chan Tonce sync.Once}func (m *MyChannel) SafeClose() {// 保证只关闭一次channelm.once.Do(func() {close(m.c)})}
实现原理
sync.Once 源码:
type Once struct {done uint32m Mutex}// 大写 只对外暴露func (o *Once) Do(f func()) {if atomic.LoadUint32(&o.done) == 0 {// 原子获取 done 的值,判断 done 的值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。o.doSlow(f)}}func (o *Once) doSlow(f func()) {// 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。o.m.Lock()defer o.m.Unlock()if o.done == 0 {// 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为 1。defer atomic.StoreUint32(&o.done, 1)f()}}
sync.once 仅提供了一个Do()方法,其参数为待执行的函数,该函数的代码块期望仅被执行一次。下面看代码实现过程:
首先,atomic读取done字段值是否被改变,然后当如果没有改变时执行doSlow方法。当进入doSlow方法,开始执行锁操作,在并发环境下仅有一个线程被执行,然后基于done字段是否被改变执行待执行函数,如果没有改变则执行f函数。当代码块执行后,done字段被激活。
