詳解併發編程之sync.Once的實現(附上三道面試題)

原文連接:戳這裏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字段作原子操做,標識該代碼塊已經被執行過了.

優化sync.Once

若是讓你本身寫一個這樣的庫,你會考慮的這樣全面嗎?相信聰明的大家也必定會寫出這樣一段代碼。若是要是我來寫,上面的代碼可能都同樣,可是在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,一名普普統統的程序猿,讓咱們一塊兒慢慢變強吧。歡迎各位的關注,咱們下期見~~~

推薦往期文章:

相關文章
相關標籤/搜索