1

一、介绍

sync.Once是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。作用与 init 函数类似,但有区别。

  • init函数是package包被首次加载的时候,初始化使用。
  • sync.Once可以在任意位置,全局中执行一次。

sync.Once源码在 src/sync/once.go
我们用 go doc sync.once 来整体看一下

type Once struct {
}
func (o *Once) Do(f func())

可以看到只有一个 sync.Once 结构体,以及一个执行的方法 Once.Do().

二、使用

func TestOnce(t *testing.T) {
    //1 申明一个 sync.Once变量
    var once sync.Once

    for i := 0; i < 10; i++ {
        go func() {
            //2 使用Do()执行首次仅只有一次操作
            once.Do(func() {
                fmt.Println("hi")
            })
        }()
    }
    time.Sleep(time.Second)
}

备注:需要注意sync.Once的变量范围,比如这个变量范围在 TestOnce()函数里面,所以在其他的地方,还是可以调用fmt.Println("hi")函数的。仅是在这个函数里,调用一次。
这里的仅执行一次是在 sync.Once 变量范围内,只执行一次。

三、源码阅读

1、如何设计一个变量对象只能让读一次?

我们只需要定义一个变量 比如done uint32类型 ,每次使用的时候,查看一下done的值,如果为0就就可以执行,执行过了,我们就把值变成1,这样就能保证只能执行一次了。

2、官方源码的sync.Once

type Once struct {
    // done 表示该操作是否执行.
    // 将done放在结构体的首位,是因为在结构体done是最热数据
    // The hot path is inlined at every call site.
    // 将done放在首位可以让指令更紧凑
    // and fewer instructions (to calculate offset) on other architectures.
    done uint32
    m    Mutex
}

加了Mutex锁,是为了在高并发情况下,能够真正的只执行一次。

3、自己实现一下Do函数

func (o *Once) Do(f func()) {
    o.mu.Lock()
    defer o.mu.Unlock()
    if o.done == 0 {
        f()
        o.done = 1
    }
}

我们这样写,写一个测试函数

func TestOnce(t *testing.T) {
    //1 申明一个 sync.Once变量
    var once Once

    for i := 0; i < 10; i++ {
        go func() {
            //2 使用Do()执行首次仅只有一次操作
            once.Do(func() {
                fmt.Println("hi")
            })
        }()
    }
    time.Sleep(time.Second)
}

输出如下:

hi

只输出一次 "hi" 说明代码没有问题。

3.1、锁的位置优化

目前的锁是一进入函数,就加锁,能否优化有以下,在写入的时候再加锁?

func (o *Once) Do(f func()) {
    if o.done == 0 {
        o.mu.Lock()            //把锁移动到这里
        defer o.mu.Unlock() //把锁移动到这里
        f()
        o.done = 1
    }
}

我们把锁移动到,写入的时候再加锁,此时试验一下。

func TestOnce(t *testing.T) {
    //1 申明一个 sync.Once变量
    var once Once

    for i := 0; i < 10; i++ {
        go func() {
            //2 使用Do()执行首次仅只有一次操作
            once.Do(func() {
                fmt.Println("hi")
            })
        }()
    }
    time.Sleep(time.Second)
}

输出如下:

=== RUN   TestOnce
hi
hi
hi
hi
hi
hi
hi
hi
hi
hi
--- PASS: TestOnce (1.00s)

发现在并发情况下有问题,原因在于,多个协程读取的时候,没有锁,此时都是0,都拿到然后自己去执行了。

谢谢您的观看,欢迎关注我的公众号。

image.png


海生
104 声望32 粉丝

与黑夜里,追求那一抹萤火。