sync.Once
草稿0.0
sync.Once
Go
语言通过sync
包可以方便的实现线程安全的单利模式。最叹为观止的是,sync
包的实现如此简单。
// Once is an object that will perform exactly one action.
type Once struct {
m Mutex
done uint32
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
问题用法
下面声明一个获取计算签名
的配置包,通过name
来获取对应的值。获取是一个Lazy Initialization
的过程,在需要使用的时候才会初始化config
变量。
package encrypt_config
//key-secret pairs
var config map[string]string
func loadConfig(name string) string {
if config == nil {
config = map[string]string{
"zi-ru": "Mji9##a0LY",
"baidu": "Kj8*0okhHH",
}
}
return config[name]
}
上述代码最显而易见的问题:并发的情况下,包内变量config
被初始化多次。因为各个goroutine
访问config
时可能都是nil
。但还存在一种可能导致错误:某一个goroutine
发现config != nil
,但是当通过name
去获取对应的值时,返回的却是空字符串。
reason
下面是在看双重检查锁定与延迟初始化
时的一段内容:
根据《The Java Language Specification, Java SE 7 Edition》(后文简称为 java 语言规范),所有线程在执行 java 程序时必须要遵守 intra-thread semantics。intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。换句话来说,intra-thread semantics 允许那些在单线程内,不会改变单线程程序执行结果的重排序。
Go
在执行上述方法的时候,也会有同样的情况发生。编译器在不影响最终结果的情况下,本身可以调整代码执行的顺序。这也是现在计算机多核处理,共享内存必然回面临的情况。上述代码最终可能会被分成多步,所以在第一个goroutine
开始初始化但还没完成的时候,其他goroutine
访问到的可能不是一个完成初始化后的结果。
func loadConfig(name string) string {
if config == nil {
config = make(map[string]string)
config["zi-ru"] = "Mji9##a0LY"
config["baidu"] = "Kj8*0okhHH"
}
return config[name]
}
问题的本质在于:其他goroutine
访问到了第一个goroutine
正在初始化的变量。而sync
通过声明done
标识,在配合Mutex
锁,巧妙的实现了隔离。
lock
// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
......
参考文章: