首先:保證變量?jī)H被初始化一次,需要有個(gè)標(biāo)志來(lái)判斷變量是否已初始化過(guò),若沒(méi)有則需要初始化。
第二:線程安全,支持并發(fā),無(wú)疑需要互斥鎖來(lái)實(shí)現(xiàn)。
package sync
import (
"sync/atomic"
)
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// 快速路經(jīng)檢測(cè),無(wú)鎖情況下保證原子性和跨CPU可見(jiàn)性
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
// mutex 能保證解鎖后其他協(xié)程對(duì)done的修改的可見(jiàn)性,所以不需要atomic方法
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
為什么將 done 置為 Once 的第一個(gè)字段?
done 在熱路徑中,done 放在第一個(gè)字段,能夠減少 CPU 指令,也就是說(shuō),這樣做能夠提升性能。
熱路徑(hot path)是程序非常頻繁執(zhí)行的一系列指令,sync.Once 絕大部分場(chǎng)景都會(huì)訪問(wèn) o.done,在熱路徑上是比較好理解的,如果 hot path 編譯后的機(jī)器碼指令更少,更直接,必然是能夠提升性能的。
為什么放在第一個(gè)字段就能夠減少指令呢?因?yàn)榻Y(jié)構(gòu)體第一個(gè)字段的地址和結(jié)構(gòu)體的指針是相同的,如果是第一個(gè)字段,直接對(duì)結(jié)構(gòu)體的指針解引用即可。如果是其他的字段,除了結(jié)構(gòu)體指針外,還需要計(jì)算與第一個(gè)值的偏移(calculate offset)。在機(jī)器碼中,偏移量是隨指令傳遞的附加值,CPU 需要做一次偏移值與指針的加法運(yùn)算,才能獲取要訪問(wèn)的值的地址。因?yàn)?,訪問(wèn)第一個(gè)字段的機(jī)器代碼更緊湊,速度更快。