sync.Once是让函数方法只被调用执行一次的实现,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接等。

图片

sync.Once的单例模式示例

 `1package main`
 `2`
 `3import (`
 `4    "fmt"`
 `5    "sync"`
 `6)`
 `7`
 `8type Instance struct{}`
 `9`
`10var (`
`11    once     sync.Once`
`12    instance *Instance`
`13)`
`14`
`15func NewInstance() *Instance {`
`16    once.Do(func() {`
`17        instance = &Instance{}`
`18        fmt.Println("Inside")`
`19    })`
`20    fmt.Println("Outside")`
`21    return instance`
`22}`
`23`
`24func main() {`
`25    for i := 0; i < 3; i++ {`
`26        _ = NewInstance()`
`27    }`
`28}`

输出

`1$ go run main.go` 
`2Inside`
`3Outside`
`4Outside`
`5Outside`

从上述例子可以看到,虽然多次调用NewInstance()函数,但是Once.Do()中的方法有且仅被执行了一次。那么sync.Once是如何做到这一点的呢?

图片

sync.Once的源码解析

`1type Once struct {`
`2    // done indicates whether the action has been performed.`
`3    // It is first in the struct because it is used in the hot path.`
`4    // The hot path is inlined at every call site.`
`5    // Placing done first allows more compact instructions on some architectures (amd64/x86),`
`6    // and fewer instructions (to calculate offset) on other architectures.`
`7    done uint32`
`8    m    Mutex`
`9}`

Once结构体非常简单,其中done是调用标识符,Once对象初始化时,其done值默认为0,Once仅有一个Do()方法,当Once首次调用Do()方法后,done值变为1。m作用于初始化竞态控制,在第一次调用Once.Do()方法时,会通过m加锁,以保证在第一个Do()方法中的参数f()函数还未执行完毕时,其他此时调用Do()方法会被阻塞(不返回也不执行)。

Once.Do()方法的实现细节如下

 `1func (o *Once) Do(f func()) {`
 `2    // Note: Here is an incorrect implementation of Do:`
 `3    //`
 `4    //  if atomic.CompareAndSwapUint32(&o.done, 0, 1) {`
 `5    //      f()`
 `6    //  }`
 `7    //`
 `8    // Do guarantees that when it returns, f has finished.`
 `9    // This implementation would not implement that guarantee:`
`10    // given two simultaneous calls, the winner of the cas would`
`11    // call f, and the second would return immediately, without`
`12    // waiting for the first's call to f to complete.`
`13    // This is why the slow path falls back to a mutex, and why`
`14    // the atomic.StoreUint32 must be delayed until after f returns.`
`15`
`16    if atomic.LoadUint32(&o.done) == 0 {`
`17        // Outlined slow-path to allow inlining of the fast-path.`
`18        o.doSlow(f)`
`19    }`
`20}`
`21`
`22func (o *Once) doSlow(f func()) {`
`23    o.m.Lock()`
`24    defer o.m.Unlock()`
`25    if o.done == 0 {`
`26        defer atomic.StoreUint32(&o.done, 1)`
`27        f()`
`28    }`
`29}`

Do()方法的入参是一个无参数输入与返回的函数,当o.done值为0时,执行doSlow()方法,为1则退出Do()方法。doSlow()方法很简单:加锁,再次检查o.done值,执行f(),原子操作将o.done值置为1,最后释放锁。

注意事项

1. 在官方示例代码中,提到了一种错误实现Do()方法的方式。

`1func (o *Once) Do(f func()) {`
`2    if atomic.CompareAndSwapUint32(&o.done, 0, 1) {`
`3        f()`
`4    }`
`5}`

当并发多次调用Do()方法时,第一个被执行的Do()方法会将o.done值从0置为1,并执行f(),其他的调用Do()方法会立即被返回。这种处理方式和加锁的方式会有所不同:它不能保证在第一个调用执行Do()方法中的f()函数被执行完毕之前,其他的f()函数会阻塞等待。

 `1package main`
 `2`
 `3import (`
 `4    "fmt"`
 `5    "sync"`
 `6    "time"`
 `7)`
 `8`
 `9type Config struct {}`
`10`
`11func (c *Config) init(filename string) {`
`12    fmt.Printf("mock [%s] config initial done!\n", filename)`
`13}`
`14`
`15var (`
`16    once sync.Once`
`17    cfg  *Config`
`18)`
`19`
`20func main() {`
`21    cfg = &Config{}`
`22`
`23    go once.Do(func() {`
`24        time.Sleep(3 * time.Second)`
`25        cfg.init("first file path")`
`26    })`
`27`
`28    time.Sleep(time.Second)`
`29    once.Do(func() {`
`30        time.Sleep(time.Second)`
`31        cfg.init("second file path")`
`32    })`
`33    fmt.Println("运行到这里!")`
`34    time.Sleep(5 * time.Second)`
`35}`

输出

`1$ go run main.go` 
`2mock [first file path] config initial done!`
`3运行到这里!`

可以看到第二次调用once.Do()时候,其输入参数f()函数虽然没有被执行,但是整个Do()是被阻塞的(被阻塞于o.m.Lock()处),它需要等待首次调用once.Do()执行完毕,才会退出阻塞状态。而错误实现Do()方法的方式,就无法保证此规则的实现。

2. 避免死锁

 `1package main`
 `2`
 `3import (`
 `4    "fmt"`
 `5    "sync"`
 `6)`
 `7`
 `8func main() {`
 `9    once := sync.Once{}`
`10    once.Do(func() {`
`11        fmt.Println("outside call")`
`12        once.Do(func() {`
`13            fmt.Println("inside call")`
`14        })`
`15    })`
`16}`

输出

`1$ go run main.go` 
`2outside call`
`3fatal error: all goroutines are asleep - deadlock!`

注意,同样由于o.m.Lock()处的代码限定,once.Do()内部调用Do()方法时,会造成死锁的发生。


推荐阅读

学习交流 Go 语言,扫码回复「进群」即可

图片

站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验

Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注

图片


好文收藏
38 声望6 粉丝

好文收集