sync.Once-保证运行期间的某段代码只会执行一次

初入门径


sync.Once提供了保证某个操作只被执行一次的功能,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接,以及并发访问只需要初始化一次的共享资源。

单例模式有懒汉模式饿汉模式两种

饿汉模式 顾名思义就是比较饥饿,所以一上来(服务启动时)就初始化。
懒汉模式 顾名思义就是偷懒,在获取实例的时候在进行初始化,但懒汉模式会有并发问题:有可能多个 goruntine 同时获取 对象都是 nil ,然后都开始创建了实例,就不满足单例模式了。解决办法是加锁

sync.Once就是懒汉模式

Go并发编程 — sync.Once

package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    fun1 := func() {
        fmt.Println("一只老虎")
    }
    once.Do(fun1)

    fun2 := func() {
        fmt.Println("两只老虎")
    }

    once.Do(fun2)
}

输出为:

一只老虎


并发调用 Do()

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var once sync.Once
    for i := 0; i < 5; i++ {
        go func(i int) {
            fun1 := func() {
                fmt.Printf("i:=%d\n", i)
            }
            once.Do(fun1)
        }(i)
    }

    // 为防止主goroutine直接运行完,什么都看不到
    time.Sleep(50 * time.Millisecond)
}

在上面这段代码里,开启了5个并发的 goroutine ,不管执行多少次, 始终只打印一次. 至于 i 是多少,就看先执行的是哪个 g 了。

Once 保证只有第一次调用 Do() 方法时, 传递的 f (无参数无返回值的函数) 才会执行,并且之后不管调用的参数是否改变了,也不管f执行时是否发生了panic,之后都不再执行。


这段是官方Demo:

 package main

 import (
     "fmt"
    "sync"
 )
 
 func main() {
     var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }
}
// 结果只打印一次:only once




源码实现


其代码实现很简洁, 从头到尾加注释不超过 70 行. 对外暴露了一个唯一接口 Do(f func()), 使用起来也很简单,可以多次调用,但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数。



点击查看 sync.Once源码:

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sync

import (
    "sync/atomic"
)

// Once is an object that will perform exactly one action.
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用来表示当前的操作是否已经被执行, 0表示还没有执行过, 1表示已经执行
    // done属性放在结构体的第一位,是因为它在hot path中使用
    // hot path在每个调用点会被内联
    // 将done放在结构体首位,像amd64/386等架构上可以允许更多的压缩指令,且在其他架构上更少的指令去计算偏移量
    done uint32
    m    Mutex
}

// 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.
//

// Do的作用主要是针对初始化且有且只能执行一次的场景。因为Do直到f返回才返回,`
// 所以如果f内再调用Do则会导致死锁
// 如果f执行过程中panic了 那么Do任务f已经执行完毕 未来再次调用不会再执行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.
        // 原子获取 done 的值,判断 done 值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。
        // 可能会存在并发 进入slow-path
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    // 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 { //二次判断f是否已经被执行
         // 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为 1。

         //即使此时有多个 goroutine 同时进入了 doSlow 方法,因为双检查的机制,后续的 goroutine 会看到 o.done 的值为 1,也不会再次执行 f
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}


Once 结构体中包含两个字段, 分别是 uint32 类型的 done 和 Mutex 类型的 m.

Once 实现了两个方法, 分别是 DodoSlow。其中 doSlow 是一个非可导出方法,只能在内部被 Do 方法调用.

Done 方法先原子获取 done 的值,如果 done 的值为 0,则调用 doSlow 方法进行二次检查,二次检查时,持有互斥锁,保证只有一个 goroutine 执行操作.如果二次检查的结果为 0,则认为是第一次执行,程序执行函数类型的参数 f,然后将 done 的值设置为 1.




注意事项


package main

import (
    "fmt"
    "sync"
)

func main() {

    panicDo()

    //nestedDo()
    nestedDo2()

}

func panicDo() {
    once := &sync.Once{}
    defer func() {
        if err := recover(); err != nil {
            once.Do(func() {
                fmt.Println("run in recover")
            })
        }
    }()
    once.Do(func() {
        panic("panic i=0")
    })
}

func nestedDo() {
    once := &sync.Once{}
    once.Do(func() {
        once.Do(func() {
            fmt.Println("test nestedDo")
        })
    })
}

func nestedDo2() {
    once1 := &sync.Once{}
    once2 := &sync.Once{}
    once1.Do(func() {
        once2.Do(func() {
            fmt.Println("test nestedDo")
        })
    })
}

详解并发编程之sync.Once的实现(附上三道面试题)

(1). sync.Once()方法中传入的函数发生了panic,重复传入还会执行吗?

执行panicDo方法,不会打印任何东西. sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic;


(2). sync.Once()方法传入的函数中再次调用sync.Once()方法会有什么问题吗?

会发生死锁! 执行nestedDo方法,会报 fatal error: all goroutines are asleep - deadlock! 根据源码实现,可知在第二个do方法会一直等doshow()中锁的释放导致发生了死锁;


(3). 执行nestedDo2,会输出什么?

会打印出 test nestedDo. once1,once2是两个对象,互不影响. 所以sync.Once是使方法只执行一次对象的实现。




官方库或知名项目中的使用


net系统的网络配置 就是存放在一个变量里,用sync.Once控制读写


当且仅当第一次读某个变量时,进行初始化(写操作)

变量被初始化过程中,所有读都被阻塞(读操作;当变量初始化完成后,读操作继续进行

变量仅初始化一次,初始化完成后驻留在内存里

Golang package sync 剖析(一): sync.Once




参考:

你真的了解 sync.Once 吗

深入源码分析golang之sync.Once

本文由mdnice多平台发布

你可能感兴趣的:(后端)