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

前言

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

單例模式🌰

自己搞得單例模式

 1type SingletonPattern struct {
 2    done bool
 3}
 4
 5func (receiver *SingletonPattern) Do(f func())  {
 6    if !receiver.done {
 7        f()
 8        receiver.done=true
 9    }
10}
11
12

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

標準庫真香系列之sync.Once

1type Once struct {
2    // 標記符號,用於標記是否執行過
3    done uint32
4    // 互斥鎖,用於保護併發調用以及防止copy
5    m    Mutex
6}
7
8
 1func (o *Once) Do(f func()) {
 2    // 原子獲取當前 done 字段是否等於0
 3    // 如果當前字段等於1 
 4    // 則代表已經 執行過
 5    // 這是第一層校驗
 6    if atomic.LoadUint32(&o.done) == 0 {
 7    // 如果爲0則代表沒被調用過則調用
 8    // 此處寫成一個函數的原因是爲了
 9    // 進行函數內聯提升性能
10        o.doSlow(f)
11    }
12}
13
14func (o *Once) doSlow(f func()) {
15    // 此處加鎖用於防止其他goroutine同時訪問調用
16    o.m.Lock()
17    defer o.m.Unlock()
18    // 二次校驗
19    // 爲的是防止多個goroutine進入此函數的時候,可能發生的重複執行 f()
20    if o.done == 0 {
21        // 函數執行結束設置done 字段爲 1代表已經執行完畢
22        defer atomic.StoreUint32(&o.done, 1)
23        // 執行
24        f()
25    }
26}
27
28

sync.Once 的使用問題

哪來的 deadlock?

fatal error: all goroutines are asleep - deadlock!

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

哪來的 invalid memory address or nil pointer dereference?

 1func main() {
 2    var once sync.Once
 3    var conn net.Conn
 4    once.Do(
 5        func() {
 6            var err error
 7            conn, err = net.Dial("tcp", "")
 8            if err != nil {
 9                return
10            }
11        })
12    conn.RemoteAddr()
13}
14
15

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

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

1conn, err = net.Dial("tcp", "")
2
3

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

 1type Once struct {
 2    once sync.Once
 3}
 4
 5func (receiver *Once) OnceDo(f func() error) error {
 6    var err error
 7    receiver.once.Do(
 8        func() {
 9            err = f()
10        })
11    return err
12}
13
14func main() {
15    var once Once
16    var conn net.Conn
17    err := once.OnceDo(
18        func() error {
19            var err error
20            conn, err = net.Dial("tcp", "")
21            if err != nil {
22                return err
23            }
24            return nil
25        })
26    if err != nil {
27        log.Fatal(err)
28    }
29}
30
31

經過封裝,我們就可以得到sync.Once 執行時是否出錯,以適配各種錯誤處理。

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

總結

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/f1CttKRRpPSX4fkEDWNw9g