在咱們開發過程當中常常會使用到單例模式這一經典的設計模式,單例模式能夠幫助開發者針對某個(些)變量或者對象或者函數(方法)進行在程序運行期間只有一次的初始化或者函數調用操做,好比在開發項目中針對某一類鏈接池的初始化(如數據庫鏈接池等)。針對這種狀況,咱們就須要使用單例模式進行操做。
要實現一個單例模式,咱們會很快就想到了在一個結構體中放置一個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
的使用問題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
對象。(重要的話要說三遍)
看一下下面的代碼:
`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
的用法以及源碼解析就完成了,可能有些地方有些理解上的錯誤,請各位諒解而且幫忙指出修改意見,若是這篇文章能幫到你,這是個人榮幸。