Go sync.Once:简约而不简单的并发利器
作者:陈明勇
专注分享后端知识,如果本文对你有帮助,不妨点个赞,如果你是 Go 语言初学者,不妨点个关注,一起成长一起进步,如果本文有错误的地方,欢迎指出!
简介
在某些场景下,我们需要初始化一些资源,例如单例对象、配置等。实现资源的初始化有多种方法,如定义 package
级别的变量、在 init
函数中进行初始化,或者在 main
函数中进行初始化。这三种方式都能确保并发安全,并在程序启动时完成资源的初始化。
然而,有时我们希望采用延迟初始化的方式,在我们真正需要资源的时候才进行初始化,这种需要确保并发安全,在这种情况下,Go
语言中的 sync.Once
提供一个优雅且并发安全的解决方案,本文将对其进行介绍。
sync.Once 基本概念
什么是 sync.Once
sync.Once
是 Go
语言中的一种同步原语,用于确保某个操作或函数在并发环境下只被执行一次。它只有一个导出的方法,即 Do
,该方法接收一个函数参数。在 Do
方法被调用后,该函数将被执行,而且只会执行一次,即使在多个协程同时调用的情况下也是如此。
sync.Once 的应用场景
sync.Once 主要用于以下场景:
单例模式:确保全局只有一个实例对象,避免重复创建资源。
延迟初始化:在程序运行过程中需要用到某个资源时,通过
sync.Once
动态地初始化该资源。只执行一次的操作:例如只需要执行一次的配置加载、数据清理等操作。
sync.Once 应用实例
单例模式
在单例模式中,我们需要确保一个结构体只被初始化一次。使用 sync.Once
可以轻松实现这一目标。
上述代码中,GetInstance
函数通过 once.Do()
确保 instance
只会被初始化一次。在并发环境下,多个协程同时调用 GetInstance
时,只有一个协程会执行 instance = &Singleton{}
,所有协程得到的实例 s
都是同一个。
延迟初始化
有时候希望在需要时才初始化某些资源。使用 sync.Once
可以实现这一目标。
在这个示例中,定义了一个 Config
结构体,它包含一些设置信息。使用 sync.Once
来实现 GetConfig
函数,该函数在第一次调用时初始化 Config
。这样,我们可以在真正需要时才初始化 Config
,从而避免不必要的开销。
sync.Once 实现原理
sync.Once
结构体包含两个字段:done
和 mu
。done
是一个 uint32
类型的变量,用于表示操作是否已经执行过;m
是一个互斥锁,用于确保在多个协程访问时,只有一个协程能执行操作。
sync.Once
结构体包含两个方法:Do
和 doSlow
。Do
方法是其核心方法,它接收一个函数参数 f
。首先它会通过原子操作atomic.LoadUint32
(保证并发安全) 检查 done
的值,如果为 0,表示 f
函数没有被执行过,然后执行 doSlow
方法。
在 doSlow
方法里,首先对互斥锁 m
进行加锁,确保在多个协程访问时,只有一个协程能执行 f
函数。接着再次检查 done
变量的值,如果 done
的值仍为 0,说明 f
函数没有被执行过,此时执行 f
函数,最后通过原子操作 atomic.StoreUint32
将 done
变量的值设置为 1。
为什么会封装一个 doSlow 方法
doSlow
方法的存在主要是为了性能优化。将慢路径(slow-path
)代码从 Do
方法中分离出来,使得 Do
方法的快路径(fast-path
)能够被内联(inlined
),从而提高性能。
为什么会有双重检查(double check)的写法
从源码可知,存在两次对 done
的值的判断。
第一次检查:在获取锁之前,先使用原子加载操作
atomic.LoadUint32
检查done
变量的值,如果done
的值为 1,表示操作已执行,此时直接返回,不再执行doSlow
方法。这一检查可以避免不必要的锁竞争。第二次检查:获取锁之后,再次检查
done
变量的值,这一检查是为了确保在当前协程获取锁期间,其他协程没有执行过f
函数。如果done
的值仍为 0,表示f
函数没有被执行过。
通过双重检查,可以在大多数情况下避免锁竞争,提高性能。
加强的 sync.Once
sync.Once
提供的 Do
方法并没有返回值,意味着如果我们传入的函数如果发生 error
导致初始化失败,后续调用 Do
方法也不会再初始化。为了避免这个问题,我们可以实现一个 类似 sync.Once
的并发原语。
上述代码实现了一个加强的 Once
结构体。与标准的 sync.Once
不同,这个实现允许 Do
方法的函数参数返回一个 error
。如果执行函数没有返回 error
,则修改 done
的值以表示函数已执行。这样,在后续的调用中,只有在没有发生 error
的情况下,才会跳过函数执行,避免初始化失败。
sync.Once 的注意事项
死锁
通过分析 sync.Once
的源码,可以看到它包含一个名为 m
的互斥锁字段。当我们在 Do
方法内部重复调用 Do
方法时,将会多次尝试获取相同的锁。但是 mutex
互斥锁并不支持可重入操作,因此这将导致死锁现象。
初始化失败
这里的初始化失败指的是在调用 Do
方法之后,执行 f
函数的过程中发生 error
,导致执行失败,现有的 sync.Once
设计我们是无法感知到初始化的失败的,为了解决这个问题,我们可以实现一个类似 sync.Once
的加强 once
,前面的内容已经提供了具体实现。
小结
本文详细介绍了 Go
语言中的 sync.Once
,包括它的基本定义、使用场景和应用实例以及源码分析等。在实际开发中,sync.Once
经常被用于实现单例模式和延迟初始化操作。
虽然 sync.Once
简单而又高效,但是错误的使用可能会造成一些意外情况,需要格外小心。
总之,sync.Once
是 Go
中非常实用的一个并发原语,可以帮助开发者实现各种并发场景下的安全操作。如果遇到只需要初始化一次的场景,sync.Once
是一个非常好的选择。
版权声明: 本文为 InfoQ 作者【陈明勇】的原创文章。
原文链接:【http://xie.infoq.cn/article/7e6cbe287381bb6a5f93158ce】。文章转载请联系作者。
评论