日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

sync.Once 的前世今生

發布時間:2024/4/11 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 sync.Once 的前世今生 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

大家好,我是好久不見的薯條,上篇文章?編寫一個配置化的Kafka Proxy,讓你分鐘級別接入Kafka的閱讀量很慘淡,搞得我那段時間有點喪,可能大家還是更喜歡Golang方面的文章,也可能是那篇寫的有點搓... 這幾天北京降溫又下雨,我久違的感冒了,秋高氣爽,讀者朋友們要注意多加衣服啊,感冒還是很難受的。

這篇once的文章前前后后看了好多參考,改了好幾遍,最終出來這么個鳥樣子,個人感覺并發編程這塊水很深,因為這塊不僅涉及Golang源碼,還涉及到匯編、操作系統、甚至是硬件的知識,真是學無止境,有興趣的朋友可以查一下Read Acquire、Write Release和Golang官方的Memory Model一文。

以下是正文:


type?Resource?struct?{addr?string }var?Res?*Resource var?once?sync.Oncefunc?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.Mutexfunc?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????Mutexdone?bool }func?(o?*Once)?Do(f?func())?{o.m.Lock()defer?o.m.Unlock()if?!o.done?{o.done?=?truef()} }

在這段2010年8月15日提交的代碼中,作者借助Mutex實現Once語義,執行的時候先加一把互斥鎖,保證只有一個協程可以操作done變量,等f函數執行完解鎖。

這樣的代碼相當于mvp版本,管用,但是略顯粗糙,一個最顯而易見的缺點:每次都要執行Mutex加鎖操作,對于Once這種語義有必要嗎,是否可以先判斷一下done的value是否為true,然后再進行加鎖操作呢?

第一次進化

于是Once開始了第一次進化,這次優化改進了上面提到的問題:若Once已經初始化,那么Do內部將不會執行搶鎖操作。做這份代碼改動的哥們經過測試發現這樣改在不同核的benchmark中有92%-99%的耗時提升。

type?Once?struct?{m????Mutexdone?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-12MOVQ?ptr+0(FP),?BXMOVL?val+8(FP),?AXXCHGL?AX,?0(BX)RET

小優化三

這次的優化在StoreUint32前增加defer前綴,增加defer是保證 即使f()在執行過程中出現panic,Once仍然保證f()只執行一次,這樣符合嚴格的Once語義。

除了預防panic,defer還能解決指令重排的問題:現在CPU為了執行效率,源碼在真正執行時的順序和代碼的順序可能并不一樣,比如這段代碼中a不一定打印"hello, world",也可能打印空字符串。

var?a?string var?done?boolfunc?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) }


總結

以上是生活随笔為你收集整理的sync.Once 的前世今生的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。