乐趣区

关于后端:深入理解-syncOnce单例模式的绝佳选择

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 源码剖析:sync.Once

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

站长 polarisxu

本人的原创文章

不限于 Go 技术

职场和守业教训

Go 语言中文网

每天为你

分享 Go 常识

Go 爱好者值得关注

退出移动版