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爱好者值得关注
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。