go单例模式
Go语⾔中的单例模式
在过去的⼏年中,Go语⾔的发展是惊⼈的,并且吸引了很多由其他语⾔(Python、PHP、Ruby)转向Go语⾔的跨语⾔学习者。
在过去的很长时间⾥,很多开发⼈员和初创公司都习惯使⽤Python、PHP或Ruby快速开发功能强⼤的系统,并且⼤多数情况下都不需要担⼼内部事务如何⼯作,也不需要担⼼线程安全性和并发性。直到最近⼏年,多线程⾼并发的系统开始流⾏起来,我们现在不仅需要快速开发
功能强⼤的系统,⽽且还要保证被开发的系统能够⾜够快速运⾏。(我们真是太难了)
对于被Go语⾔天⽣⽀持并发的特性吸引来的跨语⾔学习者来说,我觉着掌握Go语⾔的语法并不是最难的,最难的是突破既有的思维定势,真正理解并发和使⽤并发来解决实际问题。
Go语⾔太容易实现并发了,以⾄于它在很多地⽅被不正确的使⽤了。
常见的错误
有⼀些错误是很常见的,⽐如不考虑并发安全的单例模式。就像下⾯的⽰例代码:
package singleton
type singleton struct {}
var instance *singleton
func GetInstance() *singleton {
if instance == nil {
instance = &singleton{}  // 不是并发安全的
}
return instance
}
在上述情况下,多个goroutine可以执⾏第⼀个检查,并且它们都将创建该singleton类型的实例并相互覆盖。⽆法保证它将在此处返回哪个实例,并且对该实例的其他进⼀步操作可能与开发⼈员的期望不⼀致。
不好的原因是,如果有代码保留了对该单例实例的引⽤,则可能存在具有不同状态的该类型的多个实例,从⽽产⽣潜在的不同代码⾏为。这也成为调试过程中的⼀个噩梦,并且很难发现该错误,因为在调试时,由于运⾏时暂停⽽没有出现任何错误,这使⾮并发安全执⾏的可能性降到了最低,并且很容易隐藏开发⼈员的问题。
激进的加锁
也有很多对这种并发安全问题的糟糕解决⽅案。使⽤下⾯的代码确实能解决并发安全问题,但会带来其他潜在的严重问题,通过加锁把对该函数的并发调⽤变成了串⾏。
var mu Sync.Mutex
func 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 uint32
m    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()) {
Unlock()
if o.done == 0 {                    // check
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
这说明我们可以借助这个实现只执⾏⼀次某个函数/⽅法,once.Do()的⽤法如下:
once.Do(func() {
/
/ 在这⾥执⾏安全的初始化
})
下⾯就是单例实现的完整代码,该实现利⽤sync.Once类型去同步对GetInstance()的访问,并确保我们的类型仅被初始化⼀次。
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
因此,使⽤sync.Once包是安全地实现此⽬标的⾸选⽅式,类似于Objective-C和Swift(Cocoa)实现dispatch_once⽅法来执⾏类似的初始化。
结论
当涉及到并发和并⾏代码时,需要对代码进⾏更仔细的检查。始终让你的团队成员执⾏代码审查,因为这样的事情很容易就会被发现。
所有刚转到Go语⾔的新开发⼈员都必须真正了解并发安全性如何⼯作以更好地改进其代码。即使Go语⾔本⾝通过允许你在对并发性知识知之甚少的情况下设计并发代码,也完成了许多繁重的⼯作。在某些情况下,单纯的依靠语⾔特性也⽆能为⼒,你仍然需要在开发代码时应⽤最佳实践。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。