深入理解 sync.Once:单例模式的绝佳选择

Go语言精选

共 538字,需浏览 2分钟

 ·

2020-08-12 02:06

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}


输出


1go 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, 01) {
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}


输出


1go 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}


输出


1go run main.go 
2outside call
3fatal error: all goroutines are asleep - deadlock!


注意,同样由于o.m.Lock()处的代码限定,once.Do()内部调用Do()方法时,会造成死锁的发生。




推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注



浏览 236
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报