原文連接: https://blog.thinkeridea.com/202101/go/exsync/once.htmlhtml
官方描述 Once is an object that will perform exactly one action
, 即 Once
是一個對象,它提供了保證某個動做只被執行一次功能,最典型的場景就是單例模式,Once
可用於任何符合 "exactly once" 語義的場景。git
在多數狀況下,sync.Once
被用於控制變量的初始化,這個變量的讀寫一般遵循單例模式,知足這三個條件:github
在標準庫中不乏有大量 sync.Once
的使用案例,在 strings
包中 replace.go
裏實現字符串批量替換功能時,須要預編譯生成替換規則,即採用不一樣的替換算法並建立相關算法實例,因 strings.Replacer
實現是線程安全且支持規則複用,在第一次解析替換規則並建立對應算法實例後,能夠併發的進行字符串替換操做,避免屢次解析替換規則浪費資源。算法
先看一下 strings.Replacer
的結構定義:數據庫
// source: strings/replace.go type Replacer struct { once sync.Once // guards buildOnce method r replacer oldnew []string }
這裏定義了 once sync.Once
用來控制 r replacer
替換算法初始化,當咱們使用 strings.NewReplacer
建立 strings.Replacer
時,這裏採用惰性算法,並無在這時進行 build
解析替換規則並建立對應算法實例,而是在執行替換時( Replacer.Replace
和 Replacer.WriteString
)進行的, r.once.Do(r.buildOnce)
使用 sync.Once
的 Do
方法保證只有在首次執行時纔會執行 buildOnce
方法,而在 buildOnce
中調用 build
解析替換規則並建立對應算法實例,在 buildOnce
中進行賦值。安全
// source: strings/replace.go func NewReplacer(oldnew ...string) *Replacer { if len(oldnew)%2 == 1 { panic("strings.NewReplacer: odd argument count") } return &Replacer{oldnew: append([]string(nil), oldnew...)} } func (r *Replacer) buildOnce() { r.r = r.build() r.oldnew = nil } func (b *Replacer) build() replacer { .... } func (r *Replacer) Replace(s string) string { r.once.Do(r.buildOnce) return r.r.Replace(s) } func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error) { r.once.Do(r.buildOnce) return r.r.WriteString(w, s) }
簡單來講,once.Do
中的函數只會執行一次,並保證 once.Do
返回時,傳入 Do
的函數已經執行完成。多個 goroutine
同時執行 once.Do
的時候,能夠保證搶佔到 once.Do
執行權的 goroutine
執行完 once.Do
後,其餘 goroutine
才能獲得返回。併發
once.Do
接收一個函數做爲參數,該函數不接受任何參數,不返回任何參數。具體作什麼由使用方決定,錯誤處理也由使用方控制,對函數初始化的結果也由使用方進行保存。app
這給出了一種錯誤處理的例子 exec.closeOnce
,exec.closeOnce
保證了重複關閉文件,永遠只執行一次,而且老是返回首次關閉產生的錯誤信息:ide
// source: os/exec/exec.go 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() }
Once
的實現很是的靈活、簡潔、高效,排除註釋部分 Once
僅用 17 行實現,且單次執行時間在 0.3ns 左右。這讓我十分敬佩,對它可謂喜好至極,但由於它的通用性,在使用 Once
時給我帶來了一些小小的負擔,這也成了我極少的使用它的緣由。函數
Once
只保證調用安全性(即線程安全以及只執行一次動做函數),可是細心的朋友必定發現了咱們每每須要配對定義 Once
和業務實例變量,極少使用的狀況下(如上述兩個例子)看起來並無什麼負擔,可是若是咱們項目中有大量實例進行管理時(通常是集中管理,便於解決依賴問題),這時就會變得有點醜陋。
一個實際的業務場景,我有一個 http
服務,它有數百個組件實例,咱們建立了一個 APP
用來管理全部實例的初始化、依賴關係,從而保證各個組件依賴其接口,相互之間進行解耦,也使得每一個組件的配置(初始化參數)、依賴易於管理,不過咱們經常對單例實例在 http
服務啓動時進行初始化,這樣避免使用 Once
,且能夠在 http
服務啓動時暴露外部依賴問題(數據庫、其它服務等)。
這個 http
服務須要不少輔助命令,每一個命令負責極少的工做,若是我在命令啓動時使用 APP
初始化全部組件,這形成了大量的資源浪費。我單獨實現一個 Command
依賴管理組件,它大量使用 Once
保證各個組件只在第一次使用時進行初始化,這給我帶來了一些困擾,我大量定義 Once
的實例,且它和具體的組件實例沒有關聯,我在使用時須要很是的當心。
使用過 go-extend/pool 中的 pool.BufferPool 的朋友若是留意其源碼的話會發現其中定義了一些 sync.Once
的實例,這相對上訴場景倒是相對少的,如下即是 pool.BufferPool 中的部分代碼:
// source: https://github.com/thinkeridea/go-extend/blob/v1.1.2/pool/buffer.go package pool import ( "bytes" "sync" ) var ( buff64 *sync.Pool buff128 *sync.Pool buff512 *sync.Pool buff1024 *sync.Pool buff2048 *sync.Pool buff4096 *sync.Pool buff8192 *sync.Pool buff64One sync.Once buff128One sync.Once buff512One sync.Once buff1024One sync.Once buff2048One sync.Once buff4096One sync.Once buff8192One sync.Once ) type pool sync.Pool // BufferPool bytes.Buffer 的 sync.Pool 接口 // 能夠直接 Get *bytes.Buffer 並 Reset Buffer type BufferPool interface { // Get 從 Pool 中獲取一個 *bytes.Buffer 實例, 該實例已經被 Reset Get() *bytes.Buffer // Put 把 *bytes.Buffer 放回 Pool 中 Put(*bytes.Buffer) } func newBufferPool(size int) *sync.Pool { return &sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, size)) }, } } // GetBuff64 獲取一個初始容量爲 64 的 *bytes.Buffer Pool func GetBuff64() BufferPool { buff64One.Do(func() { buff64 = newBufferPool(64) }) return (*pool)(buff64) }
上訴代碼中定義了 buff64One
到 buff8192One
7個 Once
的實例,且對應的存在 buff64
到 buff8192
的業務實例,我在 GetBuff64
中必須當心使用 Once
實例,避免錯誤使用致使對應的實例未被初始化,並且上訴的代碼看起來還有一些醜陋。
鑑於我對 sync.Once
靈活、簡潔、高效的喜好,不能僅僅由於它的「吝嗇」(極簡的功能)便與之訣別,促使我開啓了探尋緩和與 sync.Once
關係之路。
首先我想到的是對 sync.Once
的二次包裝,使其能夠保存一個數據,這樣我就能夠只定義 Once
的實例,由 Once
負責存儲初始化的結果。exsync.Once 這是個人第一個實驗,它的實現很是簡潔:
// source: https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go type Once struct { once sync.Once v interface{} } func (o *Once) Do(f func() interface{}) interface{} { o.once.Do(func() { o.v = f() }) return o.v }
它嵌套一個 sync.Once
實例,並覆蓋其 Do
函數,使其接收一個 func() interface{}
函數,它要求初始化函數返回其結果,結果保存在 Once.v
,每次調用 Do
它便返回本身保存的結果,這使用起來就變得簡單許多,改造以前 exec.closeOnce
例子:
type closeOnce struct { *os.File once exsync.Once } func (c *closeOnce) Close() error { return c.once.Do(c.close).(error) } func (c *closeOnce) close() interface{} { return c.File.Close() }
這減小了一個業務層的數據定義,若是包含多個數據,可使用自定義 struct
或者 []interface{}
進行數據保存, 一個簡單打開文件的例子:
type openOnce struct { file exsync.Once } func (c *openOnce) Open(name string) (*os.File, error) { f := c.file.Do(func() interface{} { f, err := os.Open(name) return []interface{}{f, err} }).([]interface{}) return f[0].(*os.File), f[1].(error) }
這看起來使初始化的代碼變得複雜了一些,對多返回值的問題暫時沒有更好的實現,我會在後續逐漸考慮這類問題的處理方式,單個值時它使我獲得一些驚喜和便捷。即便這樣我隨後發現它相對 sync.Once
的性能大幅度降低,達到10倍之多,起初我認爲是 interface
的帶來的,我馬上實現了一個 exsync.OncePointer 以期許它能夠在性能上給我一個驚喜:
// source: https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go type OncePointer struct { once sync.Once v unsafe.Pointer } func (o *OncePointer) Do(f func() unsafe.Pointer) unsafe.Pointer { o.once.Do(func() { o.v = f() }) return o.v }
使用 unsafe.Pointer
存儲實例,讓其在編譯時肯定類型,來提高其性能,使用示例以下:
type closeOnce struct { *os.File once exsync.OncePointer } func (c *closeOnce) Close() error { return *(*error)(c.once.Do(c.close)) } func (c *closeOnce) close() unsafe.Pointer { err := c.File.Close() return unsafe.Pointer(&err) }
尷尬的是這並無使其性能有極大提高,僅僅只是稍微提高一些,難道我要和 sync.Once
就此訣別,仍是湊合過……
我本已放棄優化,即便其性能極大降低,可是它仍然能夠在 3ns 內完成任務,這並不會造成瓶頸。但多少心裏仍是有些不甘,僅僅只是包裝使其保存一個值不該該致使性能降低如此嚴重,到底是什麼致使其性能如此嚴重降低的,仔細作了分析發現因爲 sync.Once
很是的高效,且代碼簡潔,我嵌套包裝使其多了一層調用,且可能致使其沒法內聯,這對一些性能不高的組件影響極小,可是像 sync.Once
這樣高效任何小小的損耗表現都十分明顯。
我直接拷貝 sync.Once
中的代碼到 exsync.Once 及 exsync.OncePointer 實現中,這讓我獲得與 sync.Once
接近的性能,exsync.OncePointer 的實現甚至老是好於 sync.Once
。
如下是性能測試的結果,其代碼位於 exsync/benchmark/once_test.go:
goos: darwin goarch: amd64 pkg: github.com/thinkeridea/go-extend/exsync/benchmark BenchmarkSyncOnce-8 1000000000 0.391 ns/op 0 B/op 0 allocs/op BenchmarkOnce-8 1000000000 0.407 ns/op 0 B/op 0 allocs/op BenchmarkOncePointer-8 1000000000 0.389 ns/op 0 B/op 0 allocs/op PASS ok github.com/thinkeridea/go-extend/exsync/benchmark 1.438s
獲得這個結果後我堅決果斷、快馬加鞭的改變了 pool.BufferPool 中的代碼,這使 pool.BufferPool 變得簡潔許多:
package pool import ( "bytes" "sync" "unsafe" "github.com/thinkeridea/go-extend/exsync" ) var ( buff64 exsync.OncePointer buff128 exsync.OncePointer buff512 exsync.OncePointer buff1024 exsync.OncePointer buff2048 exsync.OncePointer buff4096 exsync.OncePointer buff8192 exsync.OncePointer ) type bufferPool struct { sync.Pool } // BufferPool bytes.Buffer 的 sync.Pool 接口 // 能夠直接 Get *bytes.Buffer 並 Reset Buffer type BufferPool interface { // Get 從 Pool 中獲取一個 *bytes.Buffer 實例, 該實例已經被 Reset Get() *bytes.Buffer // Put 把 *bytes.Buffer 放回 Pool 中 Put(*bytes.Buffer) } func newBufferPool(size int) unsafe.Pointer { return unsafe.Pointer(&bufferPool{ Pool: sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, size)) }, }, }) } // GetBuff64 獲取一個初始容量爲 64 的 *bytes.Buffer Pool func GetBuff64() BufferPool { return (*bufferPool)(buff64.Do(func() unsafe.Pointer { return newBufferPool(64) })) }
如此對 sync.Once
進行二次封裝,使其通用性有所降低,並必定是一個好的方案,我樂於公開它,由於它在大多數時刻能夠減小使用者的負擔,使得代碼變的簡練。
後續的思考:
Once
永遠只能執行一次,是否有安全快捷的方法可使其重置。Do
函數。解決以上這些問題,可使 sync.Once
應用在更多的場景中,但勢必致使其性能有所降低,這須要一些實驗和折中處理。
轉載:
本文做者: 戚銀(thinkeridea)
本文連接: https://blog.thinkeridea.com/202101/go/exsync/once.html
版權聲明: 本博客全部文章除特別聲明外,均採用 CC BY 4.0 CN協議 許可協議。轉載請註明出處!