經過 sync.Once 學習到 Go 的內存模型

經過 Once學習 Go 的內存模型golang

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

單例模式

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 才能獲得返回 )markdown

源碼

源碼很簡單,可是這麼簡單不到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()
	}
}
複製代碼

這裏的幾個重點知識:oop

  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 後能夠及時的被取到。ui

再看第二個問題,能夠直接使用 o.done == 0 是由於使用了 Mutex 進行了鎖操做,o.done == 0 處於鎖操做的臨界區中,因此能夠直接進行比較。atom

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

到這裏是否是發現了收獲了好多,還有更厲害的。 咱們再看看爲何 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位於結構體的第一個字段,能夠經過結構體指針直接進行訪問(訪問其餘的字段須要經過偏移量計算就慢了)

相關文章
相關標籤/搜索