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爱好者值得关注
发表回复