1. 程式人生 > 程式設計 >通過 sync.Once 學習到 Go 的記憶體模型

通過 sync.Once 學習到 Go 的記憶體模型

通過 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位於結構體的第一個欄位,可以通過結構體指標直接進行訪問(訪問其他的欄位需要通過偏移量計算就慢了)