這幾天我翻了翻golang的提交記錄,發現了一條頗有意思的提交:bc593ea,這個提交看似簡單,可是引人深思。git
commit的標題是「sync: document implementation of Once.Do」,顯然是對文檔作些補充,然而奇怪的是爲何要對某個功能的實現作文檔說明呢,難道不是配合代碼+註釋就能理解的嗎?github
根據commit的描述咱們得知,Once.Do的實現問題在過去幾個月內被問了至少兩次,因此官方決定澄清:golang
It's not correct to use atomic.CompareAndSwap to implement Once.Do,
and we don't, but why we don't is a question that has come up
twice on golang-dev in the past few months.
Add a comment to help others with the same question.編程
不過這不是這個commit的精髓,真正有趣的部分是添加的那幾行註釋。安全
commit添加的內容以下:併發
乍一看可能平平無奇,然而仔細思考事後,咱們就會發現問題了。函數
衆所周知,sync.Once
用於保證某個操做只會執行一次,所以咱們首先考慮到的就是爲了併發安全加mutex,可是once對性能有必定要求,因此咱們選用原子操做。性能
這時候atomic.CompareAndSwapUint32
很天然的就會浮如今腦海裏,而下面的結構也很天然的就給出了:atom
func (o *Once) Do(f func()) { if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() } }
然而正是這種天然聯想的方案倒是官方否認的,爲何?3d
緣由很簡單,舉個例子,咱們有一個模塊,使用模塊裏的方法前須要初始化,不然會報錯:
module.go:
package module var flag = true func InitModule() { // 這個初始化模塊的方法不能夠調用兩次以上,以便於結合sync.Once使用 if !flag { panic("call InitModule twice") } flag = false } func F() { if flag { panic("call F without InitModule") } }
main.go:
package main import ( "module" "sync" "time" ) var o = &sync.Once{} func DoSomeWork() { o.Do(module.InitModule()) // 不能屢次初始化,因此要用once module.F() } func main() { go DoSomeWork() // goroutine1 go DoSomeWork() // goroutine2 time.Sleep(time.Second * 10) }
如今無論goroutine1仍是goroutine2後運行,module
都能被正確初始化,對於F
的調用也不會panic,但咱們不能忽略一種更常見的狀況:兩個goroutine同時運行會發生什麼?
咱們列舉其中一種狀況:
InitModule
開始執行InitModule
執行被跳過F()
InitModule
在goroutine1中由於某些緣由沒執行完,因此咱們不能調用F
你可能已經看出問題了,咱們沒有等到被調用函數執行完就返回了,致使了其餘goroutine得到了一個不完整的初始化狀態。
解決起來也很簡單:
f
的操做f
f
只會被調用一次了這是代碼:
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. 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() } }
從這個問題咱們能夠看到,併發編程其實並不難,咱們給出的解決方案是至關簡單的,然而難的在於如何全面的思考併發中會遇到的問題從而編寫併發安全的代碼。
golang的這個commit給了咱們一個很好的例子,同時也是一個很好的啓發。