sync.Once 是 golang裡用來實現單例的同步原語。Once 常常用來初始化單例資源,
或者併發訪問只需初始化一次的共享資源,或者在測試的時候初始化一次測試資源。
單例,就是某個資源或者物件,只能初始化一次,類似全域性唯一的變數。
一般都認為只要使用一個flag標記即可,然後使用原子操作這個flag,程式碼如下:
type XOnce struct {
done uint32
}
func (x *XOnce) Do(f func()) {
if atomic.CompareAndSwapUint32(&x.done, 0, 1) {
f()
}
}
這種方式有很大的問題,就是如果引數f執行很慢,其他呼叫Do方法的goroutine,
雖然看到done已經設定過值,標記為已執行過,但是初始化資源的函式並未執行完,
在獲取初始化資源的時候,可能會得到空的資源或者發生空指標的panic。
來看下go原始碼中是如何解決這個問題的。
type Once struct {
m sync.Mutex
done uint32
}
func (x *Once) Do(f func()) {
if atomic.LoadUint32(&x.done) == 0 {
x.doSlow(f)
}
}
func (x *Once) doSlow(f func()) {
x.m.Lock()
defer x.m.Unlock()
if x.done == 0 {
defer atomic.StoreUint32(&x.done, 1)
f()
}
}
Once類中有一個互斥鎖和一個done標記。
用併發場景來校驗一下,假設有兩個goroutine同時呼叫Do方法,並進入doSlow,此時互斥鎖的機制保證只有一個g能執行f。
同時利用雙檢查機制,再次判斷x.done是否為,如果是0,則是第一次執行,執行完畢後,將x.done置為1,最後釋放鎖。
即時第二個g被喚醒了,但是由於此時的x.done==1,也就不會在執行f了。
雙檢查機制:既保證了併發的goroutine會等待f完成,而且還不會多次執行f