原文連接:戳這裏git
哈嘍,你們好,我是asong
,這是我併發編程系列的第二篇文章. 上一篇咱們一塊兒分析了atomic
包,今天咱們一塊兒來看一看sync/once
的使用與實現.
sync.once
Go語言標準庫中的sync.Once
能夠保證go
程序在運行期間的某段代碼只會執行一次,做用與init
相似,可是也有所不一樣:github
init
函數是在文件包首次被加載的時候執行,且只執行一次。sync.Once
是在代碼運行中須要的時候執行,且只執行一次。還記得我以前寫的一篇關於go
單例模式,懶漢模式的一種實現就可使用sync.Once
,他能夠解決雙重檢鎖帶來的每一次訪問都要檢查兩次的問題,由於sync.once
的內部實現能夠徹底解決這個問題(後面分析完源碼就知道緣由了),下面咱們來看一看這種懶漢模式怎麼寫:面試
type singleton struct { } var instance *singleton var once sync.Once func GetInstance() *singleton { once.Do(func() { instance = new(singleton) }) return instance }
實現仍是比較簡單,就不細說了。算法
sync.Once
的源碼仍是不多的,首先咱們看一下他的結構:編程
// Once is an object that will perform exactly one action. type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/x86), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
只有兩個字段,字段done
用來標識代碼塊是否執行過,字段m
是一個互斥鎖。segmentfault
接下來咱們一塊兒來看一下代碼實現:設計模式
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
這裏把註釋都省略了,反正都是英文,接下來咱用中文解釋哈。sync.Once
結構對外只提供了一個Do()
方法,該方法的參數是一個入參爲空的函數,這個函數也就是咱們想要執行一次的代碼塊。接下來咱們看一下代碼流程:緩存
done
字段的值是否改變,沒有改變則執行doSlow()
方法.doslow()
方法就開始執行加鎖操做,這樣在併發狀況下能夠保證只有一個線程會執行,在判斷一次當前done
字段是否發生改變(這裏確定有朋友會感到疑惑,爲何這裏還要在判斷一次flag
?由於若是同時有兩個goroutine調用這一行代碼,一個goroutine成功CAS設置了標誌的話,就會調用f,作資源初始化或者其它的一些事情,這個執行可能會耗費一段時間。同時另一個goroutine設置不成功,它想固然的認爲另一個goroutine已經執行了f,可是實際上f可能尚未執行完,這就可能代碼併發的問題。因此這裏是爲了保證當前代碼塊已經執行完),若是未發生改變,則開始執行代碼塊,代碼塊運行結束後會對done
字段作原子操做,標識該代碼塊已經被執行過了.若是讓你本身寫一個這樣的庫,你會考慮的這樣全面嗎?相信聰明的大家也必定會寫出這樣一段代碼。若是要是我來寫,上面的代碼可能都同樣,可是在if o.done == 0
這裏我可能會採用CAS
原子操做來代替這個判斷,以下:架構
type MyOnce struct { flag uint32 lock sync.Mutex } func (m *MyOnce)Do(f func()) { if atomic.LoadUint32(&m.flag) == 0{ m.lock.Lock() defer m.lock.Unlock() if atomic.CompareAndSwapUint32(&m.flag,0,1){ f() } } } func testDo() { mOnce := MyOnce{} for i := 0;i<10;i++{ go func() { mOnce.Do(func() { fmt.Println("test my once only run once") }) }() } } func main() { testDo() time.Sleep(10 * time.Second) } // 運行結果: test my once only run once
我就說原子操做是併發編程的基礎吧,你看沒有錯吧~。併發
上面咱們也看了源碼的實現,如今咱們來看三道題,你認爲他們的答案是多少?
sync.Once()
方法中傳入的函數發生了panic
,重複傳入還會執行嗎?
func panicDo() { once := &sync.Once{} defer func() { if err := recover();err != nil{ once.Do(func() { fmt.Println("run in recover") }) } }() once.Do(func() { panic("panic i=0") }) }
sync.Once()
方法傳入的函數中再次調用sync.Once()
方法會有什麼問題嗎?
func nestedDo() { once := &sync.Once{} once.Do(func() { once.Do(func() { fmt.Println("test nestedDo") }) }) }
改爲這樣呢?
func nestedDo() { once1 := &sync.Once{} once2 := &sync.Once{} once1.Do(func() { once2.Do(func() { fmt.Println("test nestedDo") }) }) }
在本文的最把上面三道題的答案公佈一下吧:
sync.Once.Do
方法中傳入的函數只會被執行一次,哪怕函數中發生了 panic
;do
方法會一直等doshow()
中鎖的釋放致使發生了死鎖;test nestedDo
,once1,once2是兩個對象,互不影響。因此sync.Once
是使方法只執行一次對象的實現。大家都作對了嗎?
代碼已上傳:https://github.com/asong2020/...
好啦,這篇文章就到這裏啦,素質三連(分享、點贊、在看)都是筆者持續創做更多優質內容的動力!
建立了一個Golang學習交流羣,歡迎各位大佬們踊躍入羣,咱們一塊兒學習交流。入羣方式:加我vx拉你入羣,或者公衆號獲取入羣二維碼
結尾給你們發一個小福利吧,最近我在看[微服務架構設計模式]這一本書,講的很好,本身也收集了一本PDF,有須要的小夥能夠到自行下載。獲取方式:關注公衆號:[Golang夢工廠],後臺回覆:[微服務],便可獲取。
我翻譯了一份GIN中文文檔,會按期進行維護,有須要的小夥伴後臺回覆[gin]便可下載。
翻譯了一份Machinery中文文檔,會按期進行維護,有須要的小夥伴們後臺回覆[machinery]便可獲取。
我是asong,一名普普統統的程序猿,讓咱們一塊兒慢慢變強吧。歡迎各位的關注,咱們下期見~~~
推薦往期文章: