写点什么

深入浅出 Go - sync.Once 源码分析

用户头像
哈希说
关注
发布于: 2020 年 11 月 20 日
深入浅出 Go - sync.Once 源码分析

sync.Once 是 Go 标准库提供的函数,可以用于实现单例模式,确保回调函数只执行一次,那么它是怎么实现的呢?

快速入门


首先来了解下如何使用 sync.Once,它的使用方法很简单,如下


func main() {    var once sync.Once
onceFunc := func() { fmt.Println("sync once") }
for i := 0; i < 10; i++ { go func() { once.Do(onceFunc) }() }
time.Sleep(time.Second)} // Output// sync once
复制代码


可以看到只打印了一次输出

源码阅读


那么接下来我们通过源码来了解下 sync.Once 是如何实现的


type Once struct {    done uint32    m    Mutex}
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) }}
func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() }}
复制代码


sync.Once 的源码实现非常简单,采用的是双重检测锁机制 (Double-checked Locking),是并发场景下懒汉式单例模式的一种实现方式


  1. 首先判断 done 是否等于 0,等于 0 则表示回调函数还未被执行

  2. 加锁,确保并发安全

  3. 在执行函数前,二次确认 done 是否等于 0,等于 0 则执行

  4. 将 done 置 1,同时释放锁


疑问一: 为什么不使用乐观锁 CAS


不使用乐观锁 CAS 的原因,这个在 sync.Once 的源码注释中已经明确说明了


// Note: Here is an incorrect implementation of Do:////	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {//			f()//	}//// Do guarantees that when it returns, f has finished.// This implementation would not implement that guarantee:// given two simultaneous calls, the winner of the cas would// call f, and the second would return immediately, without// waiting for the first's call to f to complete.
复制代码


简单的来说就是 f() 的执行结果最终可能是不成功的,所以你会看到现在采用的是双重检测锁机制来实现,同时需要等 f() 执行完成才修改 done 值


疑问二: 为什么读取 done 值的方式没有统一


比较 done 是否等于 0,为什么有的地方用的是 atomic.LoadUint32,有的地方用的却是 o.done。主要原因是 atomic.LoadUint32 可以保证原子读取到 done 值,是并发安全的,而在 doSlow 中,已经加锁了,那么临界区就是并发安全的,使用 o.done 就可以来读取值就可以了



发布于: 2020 年 11 月 20 日阅读数: 57
用户头像

哈希说

关注

还未添加个人签名 2018.03.08 加入

还未添加个人简介

评论

发布
暂无评论
深入浅出 Go - sync.Once 源码分析