Golang package sync 剖析(一): sync.Once

前言

Go語言在設計上對同步(Synchronization,數據同步和線程同步)提供大量的支持,好比 goroutine和channel同步原語,庫層面有html

  • sync:提供基本的同步原語(好比Mutex、RWMutex、Locker)和 工具類(Once、WaitGroup、Cond、Pool、Map)
  • sync/atomic:提供原子操做(基於硬件指令compare-and-swap)

注意:__當我說「類」時,是指 Go 裏的 struct(__單身狗要有面向「對象」編程的覺悟__)。git

Go語言裏對同步的支持主要有五類應用場景:github

  1. 資源獨佔:當多個線程依賴同一份資源(好比數據),須要同時讀/寫同一個內存地址時,runtime須要保證只有一個修改這份數據,而且保證該修改對其餘線程可見。鎖和變量的原子操做爲此而設計;
  2. 生產者-消費者:在生產者-消費者模型中,消費者依賴生產者產出數據。channel(管道) 爲此而設計;
  3. 懶加載:一個資源,當且僅當第一次執行一個操做,該操做執行過程當中其餘的同類操做都會被阻塞,直到該操做完成。sync.Once爲此而設計;
  4. fork-join:一個任務首先建立出N個子任務,N個子任務所有執行完成之後,主任務蒐集結果,執行後續操做。sync.WaitGroup 爲此而設計;
  5. 條件變量:條件變量是一個同步原語,能夠同時阻塞多個線程,直到另外一個線程 1) 修改了條件; 2)通知一個(或全部)等待的線程。sync.Cond 爲此而設計;

注意:__這裏當我說」線程」時,瞭解Go的同窗能夠自動映射到 「goroutine」(協程)。golang

關於 1和2,經過官方文檔瞭解其用法和實現。本系列的主角是 sync 下的工工具類,從 sync.Once 開始。內容分兩部分:sync.Once 用法和sync.Once 實現。編程

sync.Once 用法

在多數狀況下,sync.Once 被用於控制變量的初始化,這個變量的讀寫一般遵循單例模式,知足這三個條件:網絡

  1. 當且僅當第一次讀某個變量時,進行初始化(寫操做)
  2. 變量被初始化過程當中,全部讀都被阻塞(讀操做;當變量初始化完成後,讀操做繼續進行
  3. 變量僅初始化一次,初始化完成後駐留在內存裏

在 net 庫裏,系統的網絡配置就是存放在一個變量裏,代碼以下:函數

`package net`
`var (`
 `// guards init of confVal via initConfVal`
 `confOnce sync.Once`
 `confVal  = &conf{goos: runtime.GOOS}`
`)`
`// systemConf returns the machine's network configuration.`
`func systemConf() *conf {`
 `confOnce.Do(initConfVal)`
 `return confVal`
`}`
`func initConfVal() {`
 `dnsMode, debugLevel := goDebugNetDNS()`
 `confVal.dnsDebugLevel = debugLevel`
 `// 省略部分代碼...`
`}`

上面這段代碼裏,confVal 存放數據, confOnce 控制讀寫,兩個都是 package-level 單例變量。因爲 Go 裏變量被初始化爲默認值,confOnce 能夠被當即使用,咱們重點關注confOnce.Do。首先當作員函數 Do 的定義:工具

func (o *Once) Do(f func())

Do 接收一個函數做爲參數,該函數不接受任務參數,不返回任何參數。具體作什麼由使用方決定,錯誤處理也由使用方控制。 ui

once.Sync 可用於任何符合 「exactly once」 語義的場景,好比:atom

  1. 初始化 rpc/http client
  2. open/close 文件
  3. close channel
  4. 線程池初始化

Go語言中,文件被重複關閉會報error,而 channel 被重複關閉報 panic,once.Sync 能夠保證這類事情不發生,可是不能保證其餘業務層面的錯誤。下面這個例子給出了一種錯誤處理的方式,供你們參考:

`// source: os/exec/exec.go`
`package exec`
`type closeOnce struct {`
 `*os.File`
 `once sync.Once`
 `err  error`
`}`
`func (c *closeOnce) Close() error {`
 `c.once.Do(c.close)`
 `return c.err`
`}`
`func (c *closeOnce) close() {`
 `c.err = c.File.Close()`
`}`

sync.Once 實現

sync.Once 類經過一個鎖變量和原子變量保障 exactly once語義,直接擼下源碼(爲了便於閱讀,作了簡化處理):

`package sync`
`import "sync/atomic"`
`type Once struct {`
 `done uint32`
 `m    Mutex`
`}`
`func (o *Once) Do(f func()) {`
 `if atomic.LoadUint32(&o.done) == 0 {`
 `o.m.Lock()`
 `defer o.m.Unlock()`
 `if o.done == 0 {`
 `defer atomic.StoreUint32(&o.done, 1)`
 `f()`
 `}`
 `}`
`}`

這裏 done 是一個狀態位,用於判斷變量是否初始化完成,其有效值是:

  • 0: 函數 f 還沒有執行或執行中,Once對象建立時 done默認值就是0
  • 1: 函數 f 已經執行結束,保證 f 不會被再次執行

m Mutex 用於控制臨界區的進入,保證同一時間點最多有一個 f在執行。

donem.Lock() 先後的兩次校驗都是必要的。

發散一下

在 Scala 裏,有一個關鍵詞 lazy,實現了 sync.Once 一樣的功能。具體實現上,早期版本使用了 volatile 修飾狀態變量 done,使用 synchronized 替代 m Mutex;後來,也改爲了基於CAS的方式。

使用體驗上,顯然 lazy 更香!

References

  1. Golang: sync.Once https://golang.org/pkg/sync/#...

  2. Synchronization(Computer Science) https://en.wikipedia.org/wiki...\_(computer\_science)

  3. SIP-20 - Improved Lazy Vals Initialization http://scalajp.github.io/scal...
相關文章
相關標籤/搜索