手摸手Go 單例模式與sync.Once

I leave uncultivated today, was precisely yesterday perishes tomorrow which person of the body implored。

圖片

單例模式做爲一個較爲常見的設計模式,他的定義也很簡單,將類的實例化限制爲一個單個實例。在Java的世界裏,你可能須要從懶漢模式雙重檢查鎖模式餓漢模式靜態內部類枚舉等方式中選擇一種手動擼一遍代碼,可是他們操做起來很容易一不當心就會出現bug。而在Go裏,內建提供了保證操做只會被執行一次的sync.Once,操做起來及其簡單。web

基本使用

在開發過程當中須要單例模式的場景比較常見,好比web開發過程當中,不可避免的須要跟DB打交道,而DB管理器初始化一般須要保證有且僅發生一次。那麼使用sync.Once實現起來就比較簡單了。設計模式

`var manager *DBManager`
`var once sync.Once`
`func GetDBManager()*DBManager{`
 `once.DO(func(){`
 `manager = &DBManager{}`
 `manager.initDB(config)`
 `})`
 `return manager`
`}`

能夠看到僅僅須要once.DO(func(){...})便可, 開發者只須要關注本身的初始化程序便可,單例由sync.Once來保證,極大下降了開發者的心智負擔。安全

sync.Once源碼分析

數據結構

sync.Once結構也比較簡單,只有一個uint32字段和一個互斥鎖Mutex數據結構

`// 一旦使用不容許被拷貝`
`type Once struct {`
 `// done表示當前的操做是否已經被執行 0表示尚未 1表示已經執行`
 `// done屬性放在結構體的第一位,是由於它在hot path中使用`
 `// hot path在每一個調用點會被內聯。`
 `// 將done放在結構體首位,像amd64/386等架構上能夠容許更多的壓縮指令`
 `// 而且在其餘架構上更少的指令去計算偏移量`
 `done uint32`
 `m    Mutex`
`}`

sync.Once的核心原理,是利用sync.Mutexatomic包的原子操做來完成。done表示是否成功完成一次執行。存在兩個狀態:架構

  • 0 表示當前sync.Once 的第一次DO操做還沒有成功
  • 1 表示當前sync.Once 的第一次DO操做已經完成

每次DO方法調用都會去檢查done的值,若是爲1則啥也不作;若是爲0則進入doSlow流程,doSlow很巧妙的先使用sync.Mutex。這樣若是併發場景,只有一個goroutine會搶到鎖執行下去,其餘goroutine則阻塞在鎖上,這樣的好處是若是拿到鎖的那個goroutine失敗,其餘阻塞在鎖上的goroutine就是預備隊替補上去。確保sync.Once有且僅成功執行一次的語義。併發

圖片

once flow graph 函數

好了,接下來看源碼源碼分析

操做方法

Do

Do執行函數f當且僅當對應sync.Once實例第一次調用Do。換句話說,給定var once Once,若是once.Do(f)被調用了屢次,,儘管f在每次調用的值均不一樣,但只有第一次調用會執行f。若是須要每一個函數都執行,則須要新的sync.Once實例。ui

`// Do的做用主要是針對初始化且有且只能執行一次的場景。由於Do直到f返回才返回,`
`// 因此若是f內調用Do則會致使死鎖`
`// 若是f執行過程當中panic了 那麼Do任務f已經執行完畢 將來再次調用不會再執行f`
`func (o *Once) Do(f func()) {`
 `if atomic.LoadUint32(&o.done) == 0 {//判斷f是否被執行`
 `// 可能會存在併發 進入slow-path`
 `o.doSlow(f)`
 `}`
`}`

註釋裏提到了一種不正確的Do的實現atom

`if atomic.CompareAndSwapUint32(&o.done, 0, 1) {`
 `f()`
`}`

這種實現不正確的緣由在於,沒法保證f()有且僅執行一次的語義。由於使用直接CAS來解決問題,若是同時有多個goroutine競爭執行Do那麼是能保證有且僅有一個goroutine會獲得執行機會,其餘goroutine只能默默離開。

可是若是得到執行機會的goroutine執行失敗了,那麼之後f()就在也沒有執行機會了。

那麼咱們來看看官方的實現方式

`func (o *Once) doSlow(f func()) {`
 `o.m.Lock()`
 `defer o.m.Unlock()`
 `if o.done == 0 {//二次判斷f是否已經被執行`
 `defer atomic.StoreUint32(&o.done, 1)`
 `f()`
 `}`
`}`

官方的作法就是若是多個goroutine都來競爭Do,那麼先讓一個goroutine拿到sync.Mutex的鎖,其餘的goroutine先不着急讓他們直接返回,而是都先阻塞在sync.Mutex上。若是那個拿到鎖的goroutine很不幸執行f()失敗了,那麼defer o.m.Unlock()操做會馬上喚醒阻塞的goroutine接着嘗試執行直到成功爲止。執行成功後經過defer atomic.StoreUint32(&o.done, 1)來將執行f()的大門給關閉上。

總結

有了sync.Once,相比Java或者Python實現單例更加簡單,不用殫精竭慮懼怕手抖寫出引起線程安全問題的代碼了。

若是閱讀過程當中發現本文存疑或錯誤的地方,能夠關注公衆號留言。若是以爲還能夠 幫忙點個在看😁

圖片

相關文章
相關標籤/搜索