golang的Once研究

通過 Once學習 Go 的內存模型

Once 官方描述 Once is an object that will perform exactly one action,即 Once 是一個對象,它提供了保證某個動作只被執行一次功能,最典型的場景就是單例模式。

單例模式

package main

import (
	"fmt"
	"sync"
)

type Instance struct {
	name string
}

func (i Instance) print() {
	fmt.Println(i.name)
}

var instance Instance

func makeInstance() {
	instance = Instance{"go"}
}

func main() {
	var once sync.Once
	once.Do(makeInstance)
	instance.print()
}
複製代碼

once.Do 中的函數只會執行一次,並保證 once.Do 返回時,傳入Do的函數已經執行完成。(多個 goroutine 同時執行 once.Do 的時候,可以保證搶佔到 once.Do 執行權的 goroutine 執行完 once.Do 後,其他 goroutine 才能得到返回 )

源碼

源碼很簡單,但是這麼簡單不到20行的代碼確能學習到很多知識點,非常的強悍。

package sync

import (
	"sync/atomic"
)

type Once struct {
	done uint32
	m    Mutex
}

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()
	}
}
複製代碼

這裏的幾個重點知識:

  1. Do 方法爲什麼不直接 o.done == 0 而要使用 atomic.LoadUint32(&o.done) == 0
  2. 爲什麼 doSlow 方法中直接使用 o.done == 0
  3. 既然已經使用的Lock, 爲什麼不直接 o.done = 1, 還需要 atomic.StoreUint32(&o.done, 1)

先回答第一個問題?如果直接 o.done == 0,會導致無法及時觀察 doSlow 對 o.done 的值設置。具體原因可以參考 Go 的內存模型 ,文章中提到:

Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.

To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.
複製代碼

大意是 當一個變量被多個 gorouting 訪問的時候,必須要保證他們是有序的(同步),可以使用 sync 或者 sync/atomic 包來實現。用了 LoadUint32 可以保證 doSlow 設置 o.done 後可以及時的被取到。

再看第二個問題,可以直接使用 o.done == 0 是因爲使用了 Mutex 進行了鎖操作,o.done == 0 處於鎖操作的臨界區中,所以可以直接進行比較。

相信到這裏,你就會問到第三個問題 atomic.StoreUint32(&o.done, 1) 也處於臨界區,爲什麼不直接通過 o.done = 1 進行賦值呢?這其實還是和內存模式有關。Mutex 只能保證臨界區內的操作是可觀測的 即只有處於o.m.Lock() 和 defer o.m.Unlock()之間的代碼對 o.done 的值是可觀測的。那這是 Do 中對 o.done 訪問就可以會出現觀測不到的情況,因此需要使用 StoreUint32 保證原子性。

到這裏是不是發現了收獲了好多,還有更厲害的。 我們再看看爲什麼 dong 不使用 uint8或者bool 而要使用 uint32呢?

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/x86),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}
複製代碼

目前能看到原因是:atomic 包中沒有提供 LoadUint8 、LoadBool 的操作。

然後看註釋,我們發現更爲深奧的祕密:註釋提到一個重要的概念 hot path,即 Do 方法的調用會是高頻的,而每次調用訪問 done,done位於結構體的第一個字段,可以通過結構體指針直接進行訪問(訪問其他的字段需要通過偏移量計算就慢了)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章