新知达人, Golang语言标准库 sync 包的 Once 怎么使用?

01

介绍

在 Go 语言中,sync 包有一个 Once 类型,官方文档介绍 Once 是一个只执行一次操作的对象。所以,Once 一般用于并发执行,但只需初始化一次的共享资源。

02

基本用法

Once 的使用也非常简单,Once 只有一个 Do 方法,接收一个无参数无返回值的函数类型的参数 f,不管调用多少次 Do 方法,参数 f 只在第一次调用 Do 方法时执行。

代码示例:

新知达人, Golang语言标准库 sync 包的 Once 怎么使用?

通过阅读示例代码,可以发现代码中定义了两个函数类型的变量 func1 和 func2,分别作为参数传递给两次调用的 Do 方法,执行代码,结果只打印第一次调用 Do 方法传入的 func1 参数的值。

03

实现原理

sync.Once 源码:

type Once struct {

done uint32

m Mutex

}

func (o *Once) Do(f func()) {

  if atomic.LoadUint32(&o.done) == 0 { 

    // 原子获取 done 的值,判断 done 的值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。

o.doSlow(f)

}

}

func (o *Once) doSlow(f func()) {

  // 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。

o.m.Lock()

defer o.m.Unlock()

ifo.done ==0{

    // 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为1。

defer atomic.StoreUint32(&o.done,1)

f()

}

}

通过阅读 sync.Once 的源码,可以发现 Once 结构体中包含两个字段,分别是 uint32 类型的 done 和 Mutex 类型的 m。并且 Once 实现了两个方法,分别是 Do 和 doSlow。其中 doSlow 是一个非可导出方法,只能被 Do 方法调用。

Done 方法先原子获取 done 的值,如果 done 的值为 0,则调用 doSlow 方法进行二次检查,二次检查时,持有互斥锁,保证只有一个 goroutine 执行操作,二次检查结果仍为 0,则认为是第一次执行,程序执行函数类型的参数 f,然后将 done 的值设置为 1。

04

踩坑

我们已经介绍过 Once 是一个只执行一次操作的对象,假如我们在 Do 方法中再次调用 Do 方法会怎么样呢?代码如下:

新知达人, Golang语言标准库 sync 包的 Once 怎么使用?

阅读代码,我们定义了两个函数类型的变量 func1 和 func2,其中 func1 的函数体内,调用 Do 方法并将 func2 作为参数传递给它,最后调用给定 func1 作为参数的 Do 方法,运行结果是导致程序死锁。所以,记住不要在Do 方法的给定参数中,调用 Do 方法,否则会产生死锁。

05

总结

本文开篇介绍了 Once 的官方定义和使用场景,然后结合示例代码,介绍了 Once 的基本使用,并通过阅读源码,介绍了 Once 的实现原理,最后列举了一个容易踩的「坑」。

参考资料:

https://golang.org/pkg/sync/#Once