「Golang」sync.Once用法以及源碼講解

前言

在咱們開發過程當中常常會使用到單例模式這一經典的設計模式,單例模式能夠幫助開發者針對某個(些)變量或者對象或者函數(方法)進行在程序運行期間只有一次的初始化或者函數調用操做,好比在開發項目中針對某一類鏈接池的初始化(如數據庫鏈接池等)。針對這種狀況,咱們就須要使用單例模式進行操做。

單例模式🌰

本身搞得單例模式

要實現一個單例模式,咱們會很快就想到了在一個結構體中放置一個flag字段用於標記當前的函數是否被執行過,舉個🌰:數據庫

`type SingletonPattern struct {`
 `done bool`
`}`
`func (receiver *SingletonPattern) Do(f func())  {`
 `if !receiver.done {`
 `f()`
 `receiver.done=true`
 `}`
`}`

看似很美好,可是此時,若是傳入的須要調用的函數f()會執行很長時間,好比數據庫查詢或者作一些鏈接什麼的,當別的goroutine運行到此處的時候因爲尚未執行完f(),就會發現done標記仍然是false,那麼仍然會調用一次f(),此時就違背了單例模式的初衷。設計模式

那麼如何解決上面的併發的問題呢。此時就可使用go標準庫中所提供的併發原語---sync.Once併發

標準庫真香系列之sync.Once

話很少說先上sync.Once 結構體的源代碼:tcp

`type Once struct {`
 `// 標記符號,用於標記是否執行過`
 `done uint32`
 `// 互斥鎖,用於保護併發調用以及防止copy`
 `m    Mutex`
`}`

結構體就這麼簡單,字段done用於標記是否執行過函數,至於爲何使用uint32類型,做者的理解是爲了以後使用atomic操做作的妥協,m字段值用於保護併發狀況下的情形,而且因爲繼承了Locker接口能夠經過vet校驗到其是否被複制函數

接下來看一下用於執行函數調用的Do()函數的實現:性能

`func (o *Once) Do(f func()) {`
 `// 原子獲取當前 done 字段是否等於0`
 `// 若是當前字段等於1` 
 `// 則表明已經 執行過`
 `// 這是第一層校驗`
 `if atomic.LoadUint32(&o.done) == 0 {`
 `// 若是爲0則表明沒被調用過則調用`
 `// 此處寫成一個函數的緣由是爲了`
 `// 進行函數內聯提高性能`
 `o.doSlow(f)`
 `}`
`}`
`func (o *Once) doSlow(f func()) {`
 `// 此處加鎖用於防止其餘goroutine同時訪問調用`
 `o.m.Lock()`
 `defer o.m.Unlock()`
 `// 二次校驗`
 `// 爲的是防止多個goroutine進入此函數的時候,可能發生的重複執行 f()`
 `if o.done == 0 {`
 `// 函數執行結束設置done 字段爲 1表明已經執行完畢`
 `defer atomic.StoreUint32(&o.done, 1)`
 `// 執行`
 `f()`
 `}`
`}`

此時,sync.Once 的全部源代碼已經解析完畢了(驚不驚喜,意不意外),其實sync.Once 的過程很簡單,就是根據標記進行雙重判斷肯定函數是否執行過,沒執行就執行,執行了就跳過。ui

sync.Once 的使用問題

哪來的deadlock?

sync.Once 的確很簡單,使用也很簡單,可是仍是會有使用上可能出現的一些問題好比下列代碼:atom

`func main() {`
 `var once sync.Once`
 `once.Do(`
 `func() {`
 `fmt.Println("one once do")`
 `once.Do(`
 `func() {`
 `fmt.Println("second once do")`
 `})`
 `})`
`}`

該代碼會出現什麼問題?答案是:設計

fatal error: all goroutines are asleep - deadlock!

爲何會這樣?由於內層個Do是被外層的同一個once對象所調用,因爲此時已經進入了第一個Do而且已經調用了函數,那麼此時sync.Once 中的互斥鎖字段,已經被加了鎖,此時二次加鎖就會產生死鎖。所以使用sync.Once 最重要的一點就是:*指針

不要在執行函數中,嵌套當前的sync.Once 對象 不要在執行函數中,嵌套當前的sync.Once 對象 不要在執行函數中,嵌套當前的sync.Once 對象。(重要的話要說三遍)

哪來的invalid memory address or nil pointer dereference?

看一下下面的代碼:

`func main() {`
 `var once sync.Once`
 `var conn net.Conn`
 `once.Do(`
 `func() {`
 `var err error`
 `conn, err = net.Dial("tcp", "")`
 `if err != nil {`
 `return`
 `}`
 `})`
 `conn.RemoteAddr()`
`}`

在運行時,會出現:

panic: runtime error: invalid memory address or nil pointer dereference

爲何?由於sync.Once只保證執行一次,可是不保證執行是否出錯,即我只管調用,出錯了跟我無關,上述代碼中

`conn, err = net.Dial("tcp", "")`

一定出現err!=nil的狀況,此時若是不對conn變量進行判斷爲nil,就會出現空指針異常,那麼,如何來保證他執行成功了呢,咱們須要對其進行改造

`type Once struct {`
 `once sync.Once`
`}`
`func (receiver *Once) OnceDo(f func() error) error {`
 `var err error`
 `receiver.once.Do(`
 `func() {`
 `err = f()`
 `})`
 `return err`
`}`
`func main() {`
 `var once Once`
 `var conn net.Conn`
 `err := once.OnceDo(`
 `func() error {`
 `var err error`
 `conn, err = net.Dial("tcp", "")`
 `if err != nil {`
 `return err`
 `}`
 `return nil`
 `})`
 `if err != nil {`
 `log.Fatal(err)`
 `}`
`}`

通過封裝,咱們就能夠獲得sync.Once 執行時是否出錯,以適配各類錯誤處理。

此封裝可能會有更好的解決方案,上面的方案也僅僅是一個🌰罷了。

總結

至此sync.Once 的用法以及源碼解析就完成了,可能有些地方有些理解上的錯誤,請各位諒解而且幫忙指出修改意見,若是這篇文章能幫到你,這是個人榮幸。

相關文章
相關標籤/搜索