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爱好者值得关注