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 開始。內容分兩部分:編程

  1. sync.Once 用法
  2. 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 接收一個函數做爲參數,該函數不接受任務參數,不返回任何參數。具體作什麼由使用方決定,錯誤處理也由使用方控制。工具

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

  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
  2. Synchronization(Computer Science))
  3. SIP-20 - Improved Lazy Vals Initialization

掃碼關注微信公衆號「深刻Go語言」

圖片描述

相關文章
相關標籤/搜索