之前写的一个java的单例模式
在golang中我们期望一个方法只执行一次的时候,通常会使用实例化一个sync.Once结构体,然后使用once.Do方法来包装我们需要执行的方法f。在初始化各种连接的时候非常常用。
代码示例
import (
"sync"
)
// 单例对象的结构体
type Singleton struct {
Age int
Name string
Addr string
}
var once sync.Once
// 全局变量instance
var instance *Singleton
// 执行方法
func GetInstance() *Singleton {
if instance == nil {
once.Do(func() {
instance = &Singleton{
Age: 20,
Name: "张三",
Addr: "余杭区",
}
})
}
return instance
}
上述代码的 if instance == nil
其实是非必要的,只是让逻辑看起来更加清晰。
源码剖析
once的内部实现非常简单,无异于我们自己实现的单例模式,先来看Once结构体的样子
// 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
}
结构体中包含一个标识位done和一个锁m。m没啥说的就是锁,done用来判断方法是否已经执行过,初始状态为未执行。注意这里注释提到了一个hot path,大意为使用频次非常高的指令序列。在获取结构体中的第一个字段的时候,可以直接取消引用结构体指针。而要获取后面的字段则需要加上偏移量。所以把done放在第一个字段可以提高性能。
再来看Do方法的实现
func (o *Once) Do(f func()) {
// 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.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
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()
}
}
在Do方法中先进行了atomic.LoadUint32(&o.done) == 0
的判断,如果标识位已经改变了则意味着f已经执行过了。doSlow中使用锁保证只能有一个线程执行这个方法,在锁的临界区内又进行了一次o.done==0
的判断,就是双重检查 防止等待锁的线程获得锁之后又执行了一次f(因为是在锁的临界区内,所以不需要使用atomic,直接判断即可)。
注释里提到了一个常见的错误实现
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}
这种方式不能够保证标志位改变时方法f一定执行完毕。这可能会导致外层方法判断f方法已经执行完毕但实际上并没有,造成各种未知错误(如空指针)。
注意点,要执行的方法f中不能够再次调用do,这样会导致死锁。doSlow中获取了锁,doSlow又调用了f,f又去调用doSlow获取锁。
PS:golang中没有实现可重入锁,设计者认为锁的使用应该是清晰有层次的,如果出现需要可重入锁的情况,那么你应该要修改你的代码了。
PPS:如果方法f在执行过程中发生了panic,也会视为执行完成,后续不会再执行f,这一点需要特殊注意。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)