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