<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在此前一篇文章中我們瞭解了 Golang Mutex 原理解析,今天來看一個官方給出的 Mutex 應用場景:sync.Once。
Once is an object that will perform exactly one action.
sync.Once
是 Go 標準庫提供的使函數只執行一次的實現,常應用於單例模式,例如初始化設定、保持資料庫連線等。它可以在程式碼的任意位置初始化和呼叫,因此可以延遲到使用時再執行,並行場景下是執行緒安全的。
Once 對外僅暴露了唯一的方法 Do(f func())
,f 為需要執行的函數。
// Do calls the function f if and only if Do is being called for the // first time for this instance of Once. In other words, given // var once Once // if once.Do(f) is called multiple times, only the first call will invoke f, // even if f has a different value in each invocation. A new instance of // Once is required for each function to execute. // // Do is intended for initialization that must be run exactly once. Since f // is niladic, it may be necessary to use a function literal to capture the // arguments to a function to be invoked by Do: // config.once.Do(func() { config.init(filename) }) // // Because no call to Do returns until the one call to f returns, if f causes // Do to be called, it will deadlock. // // If f panics, Do considers it to have returned; future calls of Do return // without calling f. // func (o *Once) Do(f func())
結合註釋,我們來看看 Do 方法有哪些需要注意的:
Do
方法時,才會真正執行 f
。哪怕在多次呼叫 Do
中間 f
的值有所變化,也只會被實際呼叫一次;Do
針對的是隻希望執行一次的初始化操作,由於f
是沒有引數的,如果需要傳參,可以採用包裝一層 func 的形式來實現:config.once.Do(func() { config.init(filename) })
f
的呼叫返回之前,不會返回對Do
的呼叫,所以如果f方法中又呼叫來Do方法,將會死鎖。所以不要做這樣的操作:func main() { var once sync.Once once.Do(func() { once.Do(func() { fmt.Println("hello kenmawr.") }) }) }
f
丟擲了 panic,此時Do
會認為f
已經返回,後續多次呼叫Do
也不會再觸發對 f
的呼叫。sync.Once 的場景很多,但萬變不離其宗的落腳點在於:任何只希望執行一次的操作。
基於此,我們可以發現很多具體的應用場景落地,比如某個資源的清理,全域性變數的初始化,單例模式等,它們本質都是一樣的。這裡簡單列幾個,大家可以直接參考程式碼熟悉。
很多同學可能會有疑問,我直接在 init()
函數裡面做初始化不就可以了嗎?效果上是一樣的,為什麼還要用 sync.Once,這樣還需要多宣告一個 once 物件。
原因在於:init()
函數是在所在包首次被載入時執行,若未實際使用,既浪費了記憶體,又延緩了程式啟動時間。而 sync.Once
可以在任何位置呼叫,而且是並行安全的,我們可以在實際依賴某個變數時才去初始化,這樣「延遲初始化」從功能上講並無差異,但可以有效地減少不必要的效能浪費。
我們來看 Golang 官方的 html 庫中的一個例子,我們經常使用的跳脫字串函數
func UnescapeString(s string) string
在進入函數的時候,首先就會依賴包裡內建的 populateMapsOnce
範例(本質是一個 sync.Once) 來執行初始化 entity
的操作。這裡的entity
是一個包含上千鍵值對的 map
,如果init()
時就去初始化,會浪費記憶體。
var populateMapsOnce sync.Once var entity map[string]rune func populateMaps() { entity = map[string]rune{ "AElig;": 'U000000C6', "AMP;": 'U00000026', "Aacute;": 'U000000C1', "Abreve;": 'U00000102', "Acirc;": 'U000000C2', // 省略後續鍵值對 } } func UnescapeString(s string) string { populateMapsOnce.Do(populateMaps) i := strings.IndexByte(s, '&') if i < 0 { return s } // 省略後續的實現 ... }
開發中我們經常會實現 Getter
來暴露某個非匯出的變數,這個時候就可以把 once.Do
放到 Getter
裡面,完成單例的建立。
package main import ( "fmt" "sync" ) type Singleton struct{} var singleton *Singleton var once sync.Once func GetSingletonObj() *Singleton { once.Do(func() { fmt.Println("Create Obj") singleton = new(Singleton) }) return singleton } func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() obj := GetSingletonObj() fmt.Printf("%pn", obj) }() } wg.Wait() } /*--------- 輸出 ----------- Create Obj 0x119f428 0x119f428 0x119f428 0x119f428 0x119f428 **/
一個channel如果已經被關閉,再去關閉的話會 panic,此時就可以應用 sync.Once 來幫忙。
type T int type MyChannel struct { c chan T once sync.Once } func (m *MyChannel) SafeClose() { // 保證只關閉一次channel m.once.Do(func() { close(m.c) }) }
在 sync 的原始碼包中,Once 的定義是一個 struct,所有定義和實現去掉註釋後不過 30行,我們直接上原始碼來分析:
package sync import ( "sync/atomic" ) // 一個 Once 範例在使用之後不能被拷貝繼續使用 type Once struct { done uint32 // done 表明了動作是否已經執行 m Mutex } 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() } }
這裡有兩個非常巧妙的設計值得學習,我們參照註釋來看一下:
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/386), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
sync.Once絕大多數場景都會存取o.done
,存取 done
的機器指令是處於hot path上,hot path表示程式非常頻繁執行的一系列指令。由於結構體第一個欄位的地址和結構體的指標是相同的,如果是第一個欄位,直接對結構體的指標解除參照即可,如果是其他的欄位,除了結構體指標外,還需要計算與第一個值的偏移,所以將done放在第一個欄位,則CPU減少了一次偏移量的計算,存取速度更快。
其實使用 atomic.CompareAndSwapUint32
是一個非常直觀的方案,這樣的話 Do
的實現就變成了
func (o *OnceA) Do(f func()) { if !atomic.CompareAndSwapUint32(&o.done, 0, 1) { return } f() }
這樣的問題在於,一旦出現 CAS 失敗的情況,成功協程會繼續執行 f
,但其他失敗協程不會等待 f
執行結束。而Do
的API定位對此有著強要求,當一次 once.Do
返回時,執行的 f
一定是完成的狀態。
對此,sync.Once 官方給出的解法是:
Slow path falls back to a mutex, and the atomic.StoreUint32 must be delayed until after f returns.
我們再來結合 doSlow()
看一看這裡是怎麼解決這個並行問題的:
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() } }
atomic.LoadUint32
用於原子載入地址(也就是 &o.done),返回載入到的值;f
尚未執行),就會進入 o.doSlow(f)
的慢路徑中(slow path);doSlow
使用 sync.Mutex
來加鎖,一個協程進去,其他的被阻塞在鎖的地方(注意,此時是阻塞,不是直接返回,這是和 CAS 方案最大的差別);o.m.Lock()
獲取到鎖以後,如果此時 o.done 還是 0,意味著依然沒有被執行,此時就可以放心的呼叫 f
來執行了。否則,說明當前協程在被阻塞的過程中,已經失去了呼叫f
的機會,直接返回。defer atomic.StoreUint32(&o.done, 1)
是這裡的精華,必須等到f()
返回,在 defer 裡才能夠去更新 o.done 的值為 1。Do
,值傳遞時 done
會歸0,無法起到限制一次的效果。Do
的 f
中巢狀呼叫 Do
。到此這篇關於一文解析 Golang sync.Once 用法及原理的文章就介紹到這了,更多相關 Golang sync.Once 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45