共计 4057 个字符,预计需要花费 11 分钟才能阅读完成。
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 爱好者值得关注