如何實現一個sync.Once

JonPan發表於2023-05-05

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

相關文章