Go 单例模式

最近几年go语言的增长速度非常惊人,吸引着各界人士切换到Go语言。最近有很多关于使用Ruby语言的公司切换到Go、体验Go语言、和Go的并行和并发解决问题的文章。

过去10年里,Ruby on Rails已经让众多的开发者和初创公司快速开发出强大的系统,大多数时候不需要担心他的内部是如何工作的,或者担心线程安全和并发。RoR进程很少创建线程和并行的运行一些东西。整个托管的基础建设和框架栈使用不同的方法,通过多个进程来进行并行。最近几年,像Puma这样的多线程机架式服务器开始流行,但是即使是这样,刚开始也带来了很多关于使用第三方gems和其他没有被设计为线程安全的代码的问题。

现在有很多开发者开始使用Go语言。我们需要仔细研究我们的代码,并观察代码的行为,需要以线程安全的方式代码设代码。

常识性错误

最近,我在很多Github库里看到这种类型的错误,单例模式的实现没有考虑线程安全,下面是常识性错误的代码

package singleton

type singleton struct {
}

var instance * singleton

func GetInstance() * singleton {
     if instance == nil {
        instance = &singleton{}    // <---非线程安全的
    }
     return instance
}

上面的示例中,多个go routines 会进行第一次检查并且都会创建singleton类型的实例并且互相覆盖。不能保证哪一个实例会被返回,在这个实例上更进一步的操作可能和开发者所期望的不一至。

这样是有问题的,因为如果对这个单例的实例已经在代码中被应用,可能会有潜在的多个这个类型的实例,并用有各自的状态,产生潜在的不同的代码行为。他也可能成为高度时的恶梦,并且很难定位错误,因为在debug时由于运行时暂停减少潜在的非线程安全的执行而不会真正出现错误,很容易隐藏开发者的问题。
激进的锁

我也看到一些使用糟糕的方法来解决线程安全的问题。事实上他解决了多线程的问题,但是创造了其他潜在的更严重的问题,他通过对整个方法执行锁定来引入线程竞争

var mu Sync.Mutex

func GetInstance() * singleton {
    mu.Lock()                     // <---如果实例已经被创建就没有必要锁写
    defer mu.Unlock()

    if instance == nil {
        instance = & singleton{}
    }
    return instance
}

上面的代码,我们可以看到,通过引入Sync.Mutex来解决线程安全的问题,并且在创建单例实例前获取锁。问题在于当我们不需要的时候例如,实例已经被创建的时候,只需要返回缓存的单例实例,但是呢也会执行锁操作。在高并发代码基础上,这会产生瓶颈,因为在同一时间只有一个go routine可以得到单例的实例。

所以这不是最好的方法,我们找找其他的解决方案。

Check-Lock-Check 模式

在c++和其他语言,用于保证最小锁定并且保证线程安全的最好、最安全的方式是当需要锁定时使用众所周知的Check-Lock-Check模式。下面的伪代码说明了这个模式的大概样子

if check() {
     lock () {
         if check() {
             // perform your lock-safe code here 
        }
    }
}

这个模式背后的想法是想一开始就检查。用于减少任何激进的锁定。因为一个IF语句比锁定便宜的多。第二我们想等待并获取排他锁所以的块内同一时间只能有一个执行,但是在第一次检察和和排他锁获取之间可能会有其他线程想要获取锁,因此我们需要在块内再次的检查以避免单例实例被其他实例替换。

多年来,和我一起工作人的熟知这一点,在代码审过程中,这个模式和线程安全思想方面,我对团队非常严厉。

如果我们应用这个模式到我的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语言和标准库的源码看一下go routines 同步的实现方式来做的更好

Go惯用的单例方法

我们想要使用Go的惯用手法来实现这个单例模式。所以我们需要看一下打包好的sync标准库。我们找到了Once 类型。这个对象可以精确的只执行一次操作,下面就是Go标准库的代码

// Once is an object that will perform exactly one action. 
type Once struct {
    m Mutex
    done uint32
}

 // Do calls the function f if and only if Do is being called for the
 // first time for this instance of Once. In other words, given
 //      var once Once
 // if once.Do(f) is called multiple times , only the first call will invoke f,
 // even if f has a different value in each invocation. A new instance of
 // Once is required for each function to execute.
 // 
 // Do is intended for initialization that must be run exactly once. Since f
 // is niladic, it may be necessary to use a function literal to capture the
 // arguments to a function to be invoked by Do:
 //     config.once.Do(func() { config.init(filename) })
 // 
 // Because no call to Do returns until the one call to f returns, if f causes
 // Do to be called, it will deadlock.
 // 
 // If f panics, Do considers it to have returned; future calls of Do return
 // without calling f.
 //
 func (o * Once) Do(f func()) {
     if atomic.LoadUint32(&o.done ) == 1 { // <-- Check 
        return
    }
    // Slow-path. 
    omLock()                            // <-- Lock 
    defer omUnlock()
     if o.done == 0 {                      // <-- Check 
        defer atomic.StoreUint32(&o.done, 1 )
        f()
    }
}

这意味着我们可以运用非常棒的 Go sync包来调用一个只执行一次的方法。因此,我们可以向下面这样调用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包是一个完美的安全的实现方式,这种方式有点像Object-C和Swift(Cocoa)的实现dispatch_once 方法用于执行类似的初始化操作。

总结

当涉及到并行和并发代码时,需要详细检查你的代码。始终让你的团队成员进行代码审查,因此对于这样的事情才能更容易的监督。

所有的切换到Go语言的新开发者,需要明确的理解线程安全的原理才能更好的改善你的代码。即使Go语言本身通过做了很多努力允许你使用很少的并发知识来设计并发代码。仍有一些语言无法帮你处理的一些情况,你依然需要在开发代码时应用最佳的实践方法

补充

  1. 为什么不使用 init()函数声明单例?这样更简单高效
    答: 如果想要实现懒初始化(Lazy initialization)则需要考虑同步问题
  2. sync.Once 会保证又一次调用成功之后再返回,也就是说如果多人同时调用Do函数,则只有一个人调用成功之后,其他人才会返回。
  3. 当调用函数Panic时,也会认为其已经调用完成,其它调用方也会返回。

原文地址

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值