Sync Once:不是吧,不到 20 行源碼居然來回改了這麼多次
大家好,我是好久不見的薯條,上篇文章 編寫一個配置化的 Kafka Proxy,讓你分鐘級別接入 Kafka 的閱讀量很慘淡,搞得我那段時間有點喪,可能大家還是更喜歡 Golang 方面的文章,也可能是那篇寫的有點搓... 這幾天北京降溫又下雨,我久違的感冒了,秋高氣爽,讀者朋友們要注意多加衣服啊,感冒還是很難受的。
這篇 once 的文章前前後後看了好多參考,改了好幾遍,最終出來這麼個鳥樣子,個人感覺併發編程這塊水很深,因爲這塊不僅涉及 Golang 源碼,還涉及到彙編、操作系統、甚至是硬件的知識,真是學無止境,有興趣的朋友可以查一下Read Acquire
、Write Release
和 Golang 官方的 Memory Model 一文。
以下是正文:
type Resource struct {
addr string
}
var Res *Resource
var once sync.Once
func GetResourceOnce(add string) *Resource {
once.Do(func() {
Res = &Resource{addr: add}
})
return Res
}
func main() {
fmt.Println(GetResource("beijing"))
}
// output:{beijing}
例子:
var Resp *Resource
var mut sync.Mutex
func GetResourceMutex(add string) *Resource {
mut.Lock()
defer mut.Unlock()
if Resp != nil {
return Resp
}
Resp = &Resource{addr: add}
return Resp
}
1. 爲啥源碼引入Mutex而不是CAS操作
3. 爲啥要有fast path, slow path
4. 加鎖之後爲啥要有done==0,爲啥有double check,爲啥這裏不是原子讀
4.store爲啥要加defer
5.爲啥是atomic.store,不是直接賦值1
Once 開始的地方
type Once struct {
m Mutex
done bool
}
func (o *Once) Do(f func()) {
o.m.Lock()
defer o.m.Unlock()
if !o.done {
o.done = true
f()
}
}
在這段 2010 年 8 月 15 日提交的代碼中,作者藉助Mutex
實現 Once 語義,執行的時候先加一把互斥鎖,保證只有一個協程可以操作done
變量,等f
函數執行完解鎖。
這樣的代碼相當於 mvp 版本,管用,但是略顯粗糙,一個最顯而易見的缺點:每次都要執行 Mutex 加鎖操作,對於 Once 這種語義有必要嗎,是否可以先判斷一下 done 的 value 是否爲 true,然後再進行加鎖操作呢?
第一次進化
於是 Once 開始了第一次進化,這次優化改進了上面提到的問題:若 Once 已經初始化,那麼 Do 內部將不會執行搶鎖操作。做這份代碼改動的哥們經過測試發現這樣改在不同核的 benchmark 中有 92%-99% 的耗時提升。
type Once struct {
m Mutex
done int32
}
func (o *Once) Do(f func()) {
if atomic.AddInt32(&o.done, 0) == 1 {
return
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
f()
atomic.CompareAndSwapInt32(&o.done, 0, 1)
}
}
在這段代碼中,在 slow-path 加鎖後,要繼續判斷 done 值是否爲 0,確認 done 爲 0 後纔要執行f()
函數,這是因爲在多協程環境下僅僅通過一次atomic.AddInt32
判斷並不能保證原子性,比如倆協程 g1、g2,g2 在 g1 剛剛執行完atomic.CompareAndSwapInt32(&o.done, 0, 1)
進入了 slow path,如果不進行 double check,那 g2 又會執行一次f()
。
在這次改動中,作者用一個 int32 變量done
表示 once 的對象是否已執行完,有兩個地方使用到了atomic
包裏的方法對o.done
進行判斷,分別是,用AddInt32
函數根據o.done
的值是否爲 1 判斷 once 是否已執行過,若執行過直接返回;f()
函數執行完後,對o.done
通過 cas 操作進行賦值 1。
這兩處地方的存在有一定的爭議性,在源碼 cr 的過程中就被問到atomic.CompareAndSwapInt32(&o.done, 0, 1)
可否被o.done == 1
替換, 答案是不可以。
現在的 CPU 一般擁有多個核心,而 CPU 的處理速度快於從內存讀取變量的速度,爲了彌補這倆速度的差異,現在 CPU 每個核心都有自己的 L1、L2、L3 級高速緩存,CPU 可以直接從高速緩存中讀取數據,但是這樣一來內存中的一份數據就在緩存中有多份副本,在同一時間下這些副本中的可能會不一樣,爲了保持緩存一致性,Intel CPU 使用了 MESI 協議。
AddInt32
方法和CompareAndSwapInt32
方法 (均爲 amd64 平臺 runtime/internal/atomic/atomic_amd64.s) 底層都是在彙編層面調用了LOCK
指令,LOCK
指令通過總線鎖或 MESI 協議保證原子性(具體措施與 CPU 的版本有關),提供了強一致性的緩存讀寫保證,保證LOCK
之後的指令在帶LOCK
前綴的指令執行之後才執行,從而保證讀到最新的o.done
值。
第二次進化
至此 Once 的代碼已經成型了,後面來列舉一些小優化的集合:
小優化一
這個小優化把 done 的類型由int32
替換爲uint32
, 用CompareAndSwapUint32
替換了CompareAndSwapInt32
, 用LoadUint32
替換了AddInt32
方法,LoadUint32
底層並沒有LOCK
指令用於加鎖,我覺得能這麼寫的主要原因是進入 slow path 之後會繼續用 Mutex 加鎖並判斷o.done
的值,且後面的CAS
操作是加鎖的,所以可以這麼改。這次優化經過 benchmark 測試性能在不同核心上有 45%-94% 的提升。
小優化二
這次小優化用StoreUint32
替換了CompareAndSwapUint32
操作,CAS 操作在這裏確實有點多餘,因爲這行代碼最主要的功能是原子性的done = 1
。
Store 命令的底層是,其中關鍵的指令是XCHG
,有的同學可能要問了,這源碼裏沒有LOCK
指令啊,怎麼保證 happen before 呢,Intel 手冊有這樣的描述: The LOCK prefix is automatically assumed for XCHG instruction.
,這個指令默認帶LOCK
前綴,能保證 Happen Before 語義。
TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, $0-12
MOVQ ptr+0(FP), BX
MOVL val+8(FP), AX
XCHGL AX, 0(BX)
RET
小優化三
StoreUint32
前增加 defer 前綴,增加 defer 是保證 即使f()
在執行過程中出現 panic,Once 仍然保證f()
只執行一次,這樣符合嚴格的 Once 語義。
除了預防 panic,defer 還能解決指令重排的問題:現在 CPU 爲了執行效率,源碼在真正執行時的順序和代碼的順序可能並不一樣,比如這段代碼中 a 不一定打印 "hello, world",也可能打印空字符串。
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
而增加了 defer 前綴,能保證,即使出現指令重排,done
變量也能在f()
函數執行完後才進行 store 操作。
小優化四
這次優化主要是用函數區分開了 fast path 和 slow path,對 fast path 做了內聯優化。這樣進一步降低了使用 Once 的開銷,因爲 fast path 會被內聯到使用 once 的函數調用中,每次調用的時候如果只走到 fast path 那麼連函數調用的開銷都省去了,這次優化在不同核的環境下又有54%-67%
的提升。
type St struct {
ponce *sync.Once
}
func (st *St) Reset() {
st.ponce = new(sync.Once)
}
func main() {
s := &St{}
f1 := func() {
fmt.Println("hello, world")
}
s.Reset()
s.ponce.Do(f1)
s.Reset()
s.ponce.Do(f1)
}
最後,給自己打個廣告
歡迎加入 隨波逐流的薯條 微信羣。
薯條目前有草帽羣、木葉羣、琦玉羣,羣交流內容不限於技術、投資、趣聞分享等話題。歡迎感興趣的同學入羣交流。
入羣請加薯條的個人微信:709834997。並備註:加入薯條微信羣。
歡迎關注我的公衆號~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/nkhZyKG4nrUulpliMKdgRw