1. 程式人生 > >Golang原始碼分析atomic.Once

Golang原始碼分析atomic.Once

package sync

import (
	"sync/atomic"
)
type Once struct {
	m    Mutex
	done uint32
}
func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 1 {
		return
	}
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}
原始碼很簡潔,Once結構體只有兩個元素:一個鎖定臨界區的Mutex互斥鎖,一個uint32型別的done標誌位,Once是開箱即用的(Mutex是開箱即用的,done的零值就是uint32(0))
Do方法解析:
1 在方法開始進行原子檢查done標誌位是否為1,如果滿足立即返回
2 在條件1不滿足情況下,對臨界區上鎖(寫鎖)然後檢查done的值是否為0
3 如果條件2滿足,用defer表示式聲明瞭一個語句:原子的將done的值設定為1;然後執行f函式
4 Do方法內部為什麼要做兩次done檢查:第一個原子檢查是為了快速判定done的狀態來決定是否嘗試獲取該Once的互斥鎖,畢竟獲取互斥鎖是可能阻塞的,有可能出現多個goroutine爭搶互斥鎖,減小爭搶鎖的概率和發生死鎖的概率;進入臨界區的檢查意義是:雖然能進入臨界區的goroutine顯然是直到done的值為0的,但這並不能保證接下來的操作就是安全的,因為在if判定done之後到m上鎖之間是由間歇的,這裡索然沒有程式碼間隙,但是在CPU指令排布上是可以被插入別的指令的(比如在if條件獲取了done的值並解鎖done之後,另一個goroutine剛好執行完f部分也將done狀態改為了1,釋放了m鎖,那麼當前goroutine拿到鎖之後如果不判斷done的值就不能保證其中的f函式在Do的結構裡面只會執行一次)
5 及時f函式執行過程中出現panic,defer表示式也會保證done的值原子的變為1,也就是說有可能出現某些情況下呼叫了Once.Do方法但實際上初始化並沒有成功。
6 Once結構內部元素都是包級私有的,我們無法從外部檢測到done的狀態
Once.Do方法的效果是:
1 這個方法在一個程式中只會執行一次,重複呼叫無效(只要是同一個Once)
2 Once.Do(f)方法保護的是這個呼叫表示式本身只會有效執行一次。而不是保證f只會被執行一次,在其他程式碼區域還是可以重複呼叫f函式
3 只有在f函式執行完畢後done的值才會真正變成1(已完成),標誌位done變為1後才會解鎖m(注意兩個defer表示式執行的順序),這就意味著如果f是一個阻塞的函式,那麼本次呼叫Do方法的goroutine將會一直阻塞,並且Once的狀態會保持為0直到f執行結束,如果這個時候還有別的Goroutine也在執行該Once的Do方法那麼也將阻塞(等待獲取寫鎖)