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
	}
    ......

参考文章:

  1. 双重检查锁定与延迟初始化