回到顶部
Go措辞中的单例模式在过去的几年中,Go措辞的发展是惊人的,并且吸引了很多由其他措辞(Python、PHP、Ruby)转向Go措辞的跨措辞学习者。
在过去的很永劫光里,很多开拓职员和初创公司都习气利用Python、PHP或Ruby快速开拓功能强大的系统,并且大多数情形下都不须要担心内部事务如何事情,也不须要担心线程安全性和并发性。直到最近几年,多线程高并发的系统开始盛行起来,我们现在不仅须要快速开拓功能强大的系统,而且还要担保被开拓的系统能够足够快速运行。(我们真是太难了☺️)

对付被Go措辞天生支持并发的特性吸引来的跨措辞学习者来说,我觉着节制Go措辞的语法并不是最难的,最难的是打破既有的思维定势,真正理解并发和利用并发来办理实际问题。
Go措辞太随意马虎实现并发了,以至于它在很多地方被禁绝确的利用了。
常见的缺点有一些缺点是很常见的,比如不考虑并发安全的单例模式。就像下面的示例代码:
package singletontype singleton struct {}var instance singletonfunc GetInstance() singleton {if instance == nil {instance = &singleton{} // 不是并发安全的}return instance}
在上述情形下,多个goroutine可以实行第一个检讨,并且它们都将创建该singleton类型的实例并相互覆盖。无法担保它将在此处返回哪个实例,并且对该实例的其他进一步操作可能与开拓职员的期望不一致。
不好的缘故原由是,如果有代码保留了对该单例实例的引用,则可能存在具有不同状态的该类型的多个实例,从而产生潜在的不同代码行为。这也成为调试过程中的一个噩梦,并且很难创造该缺点,由于在调试时,由于运行时停息而没有涌现任何缺点,这使非并发安全实行的可能性降到了最低,并且很随意马虎隐蔽开拓职员的问题。
激进的加锁也有很多对这种并发安全问题的糟糕办理方案。利用下面的代码确实能办理并发安全问题,但会带来其他潜在的严重问题,通过加锁把对该函数的并发调用变成了串行。
var mu Sync.Mutexfunc GetInstance() singleton { mu.Lock() // 如果实例存在没有必要加锁 defer mu.Unlock() if instance == nil { instance = &singleton{} } return instance}
在上面的代码中,我们可以看到在创建单例实例之前通过引入Sync.Mutex和获取Lock来办理并发安全问题。问题是我们在这里实行了过多的锁定,纵然我们不须要这样做,在实例已经创建的情形下,我们该当大略地返回缓存的单例实例。在高度并发的代码根本上,这可能会产生瓶颈,由于一次只有一个goroutine可以得到单例实例。
因此,这不是最佳方法。我们必须考虑其他办理方案。
Check-Lock-Check模式在C ++和其他措辞中,确保最小程度的锁定并且仍旧是并发安全的最佳和最安全的方法是在获取锁定时利用众所周知的Check-Lock-Check模式。该模式的伪代码表示如下。
if check() { lock() { if check() { // 在这里实行加锁安全的代码 } }}
该模式背后的思想是,你该当首先进行检讨,以最小化任何主动锁定,由于IF语句的开销要比加锁小。其次,我们希望等待并获取互斥锁,这样在同一时候在那个块中只有一个实行。但是,在第一次检讨和获取互斥锁之间,可能有其他goroutine获取了锁,因此,我们须要在锁的内部再次进行检讨,以避免用另一个实例覆盖了实例。
如果将这种模式运用于我们的GetInstance()方法,我们会写出类似下面的代码:
func GetInstance() singleton { if instance == nil { // 不太完美 由于这里不是完备原子的 mu.Lock() defer mu.Unlock() if instance == nil { instance = &singleton{} } } return instance}
通过利用sync/atomic这个包,我们可以原子化加载并设置一个标志,该标志表明我们是否已初始化实例。
import "sync"import "sync/atomic"var initialized uint32... // 此处省略func GetInstance() singleton { if atomic.LoadUInt32(&initialized) == 1 { // 原子操作 return instance } mu.Lock() defer mu.Unlock() if initialized == 0 { instance = &singleton{} atomic.StoreUint32(&initialized, 1) } return instance}
但是……这看起来有点繁琐了,我们实在可以通过研究Go措辞和标准库如何实现goroutine同步来做得更好。
Go措辞惯用的单例模式我们希望利用Go惯用的办法来实现这个单例模式。我们在标准库sync中找到了Once类型。它能担保某个操作仅且只实行一次。下面是来自Go标准库的源码(部分注释有编削)。
// Once is an object that will perform exactly one action.type Once struct {// done indicates whether the action has been performed.// It is first in the struct because it is used in the hot path.// The hot path is inlined at every call site.// Placing done first allows more compact instructions on some architectures (amd64/x86),// and fewer instructions (to calculate offset) on other architectures.done uint32m Mutex}func (o Once) Do(f func()) {if atomic.LoadUint32(&o.done) == 0 { // check// Outlined slow-path to allow inlining of the fast-path.o.doSlow(f)}}func (o Once) doSlow(f func()) {o.m.Lock() // lockdefer o.m.Unlock()if o.done == 0 { // checkdefer atomic.StoreUint32(&o.done, 1)f()}}
这解释我们可以借助这个实现只实行一次某个函数/方法,once.Do()的用法如下:
once.Do(func() { // 在这里实行安全的初始化})
下面便是单例实现的完全代码,该实现利用sync.Once类型去同步对GetInstance()的访问,并确保我们的类型仅被初始化一次。
package singletonimport ( "sync")type singleton struct {}var instance singletonvar once sync.Oncefunc GetInstance() singleton { once.Do(func() { instance = &singleton{} }) return instance}
因此,利用sync.Once包是安全地实现此目标的首选办法,类似于Objective-C和Swift(Cocoa)实现dispatch_once方法来实行类似的初始化。
结论当涉及到并发和并行代码时,须要对代码进行更仔细的检讨。始终让你的团队成员实行代码审查,由于这样的事情很随意马虎就会被创造。
所有刚转到Go措辞的新开拓职员都必须真正理解并发安全性如何事情以更好地改进其代码。纵然Go措辞本身通过许可你在对并发性知识知之甚少的情形下设计并发代码,也完成了许多繁重的事情。在某些情形下,纯挚的依赖措辞特性也无能为力,你仍旧须要在开拓代码时运用最佳实践。