天天看點

深入了解 sync.Once 與 sync.Pool

深入了解 sync.Once 與 sync.Pool

2021-06-24 18:24 

沉睡的木木夕 

閱讀(0) 

評論(0) 

編輯 

收藏 

舉報

深入了解 sync.Once 與 sync.Pool

sync.Once

代表在這個對象下在這個示例下多次執行能保證隻會執行一次操作。

var once sync.Once
for i:=0; i < 10; i++ {
	once.Do(func(){
		fmt.Println("execed...")
	})
}
           

在上面的例子中,once.Do 的參數 func 函數就會保證隻執行一次。

sync.Once 原理

那麼 sync.Once 是如何保證 Do 執行體函數隻執行一次呢?

從 sync.Once 的源碼就可以看出其實就是通過一個 uint32 類型的 done 辨別實作的。當

done = 1

就辨別着已經執行過了。Once 的源碼非常簡短

package sync

import (
	"sync/atomic"
)

type Once struct {
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 {
		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()
	}
}
           

Do

方法内部用到了記憶體加載同步原語

atomic.LoadUint32

done = 0

表示還沒有執行,是以多個請求在

f

執行前都會進來執行

o.doSlow(f)

,然後通過互斥鎖使保證多個請求隻有一個才能成功執行,保證了 f 成功傳回之後才會記憶體同步原語将

done

設定為 1。最後釋放鎖,後面的請求就因無法滿足判斷而退出。

如果仔細檢視源代碼中的注釋就會發現 go 團隊還解釋了為什麼沒有使用 cas 這種同步原語實作。因為

sync.Once

Do(f)

在執行的時候要保證隻有在 f 執行完之後 do 才傳回。想象一下有至少兩個請求,Do 是用 cas 實作的:

func (o *Once) Do(f func()) {
	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
		f()
	}
}
           

雖然 cas 保證了同一時刻隻有一個請求進入 if 判斷執行 f()。但是其它的請求卻沒有等待 f() 執行完成就立即傳回了。那麼使用者端在執行 once.Do 傳回之後其實就可能存在 f() 還未完成,就會出現意料之外的錯誤。如下面例子

var db SqlDb
var once sync.Once
for i:=0; i < 2; i++ {
	once.Do(func() {
         db = NewSqlDB()
        fmt.Println("execed...")
    })
}
// #1
db.Query("select * from table")
...
           

根據上述如果是用 cas 實作的 once,那麼當

once.Do

執行完傳回并且循環體結束到達 #1 時,由于 db 的初始化函數可能還沒完成,那麼這個時候 db 還是 nil,那麼直接調用

db.Query

就會發生錯誤了。

sync.Once 使用限制

由于 Go 語言一切皆 struct 的特性,我們在使用 sync.Once 的時候一定要注意不要通過傳遞參數使用。因為 go 對于 sync.Once 參數傳遞是值傳遞,會将原來的 once 拷貝過來,是以有可能會導緻 once 會重複執行或者是已經執行過了就不會執行的問題。

func main() {
	for i := 0; i < 10; i++ {
		once.Do(func() {
			fmt.Println("execed...")
		})
	}
	duplicate(once)
}

func duplicate(once sync.Once) {
	for i := 0; i < 10; i++ {
		once.Do(func() {
			fmt.Println("execed2...")
		})
	}
}
           

比如上述例子,由于 once 已經執行過一次,once.done 已經為 1。這個時候再通過傳遞,由于 once.done 已經為1,是以就不會執行了。上面的輸出結果隻會列印第一段循環的結果

execed...

sync.Pool

sync.Pool 其實把初始化的對象放到内部的一個池對象中,等下次通路就直接傳回池中的對象,如果沒有的話就會生成這個對象放入池中。Pool 的目的是”預熱“,即初始化但還未立即使用的對象,由于預先初始化至 Pool,是以到後續取得時候就直接傳回已經初始化過得對象即可。這樣提高了程式吞吐,因為有時候在運作時初始化一些對象的開銷是非常昂貴的,如資料庫連接配接對象等。

現在我們來深入分析 Pool

sync.Pool 原理

sync.Pool 核心對象有三個

  1. New:函數,負責對象初始化
  2. Get:擷取 Pool 中的對象,如果 Pool 中對象不存在則會調用 New
  3. Put:将對象放入 Pool 中

New func

Pool 的結構很簡單,就 5 個字段

type Pool struct {
	...
	New func() interface{}
}
           

字段

New

是一個初始化對象的指針,該方法不是必填的,當沒有設定 New 函數時,調用 Get 方法會傳回 nil。隻有在指定了 New 函數體後,調用 Get 如果發現 Pool 中沒有就會調用 New 初始化方法并傳回該對象。

poolLocalInternal

在将 Get、Put 之前得先了解 poolLocalInternal 這個對象,裡面隻有兩個對象,都是用來存儲要用的對象的:

type poolLocalInternal struct {
	private interface{} // Can be used only by the respective P.
	shared  poolChain   // Local P can pushHead/popHead; any P can popTail.
}
           

操作這個對象時必須要把目前的 goroutine 綁定到 P,并且禁止讓出 g。在 Get 和 Put 操作時都是優先操作

private

這個字段,隻有在這個字段為 nil 的情況下才會轉而讀取 poolChain 共享連結清單,每讀取操作都是一次 pop。

Get

每個目前 goroutine 都擁有一個

poolLocalInternal.private

,在 g 調用 Get 方法時會做如下方法:

  1. 查詢

    private

    是否有值,有直接傳回;沒有查詢共享 poolChain 連結清單
  2. 如果 poolChain 連結清單 pop 傳回的值不為 nil,則直接傳回;如果沒有值則轉向其它 P 中的 poolChain 隊列中存在的值
  3. 如果其它的 P 的共享隊列中都沒有值,就會嘗試在主存中位址擷取對應的值傳回
  4. 最終都沒有就會執行 New 函數體傳回,沒有設定 New 則傳回 nil。

從上面的調用過程來看,Pool.Get 擷取值的過程在一定程度與 gmp 模型有很多相似的地方的。

Put

Put 操作就比較簡單了,優先将值指派給

poolLocalInternal.private

(同樣是固定将目前的 G 綁定到 P 上),如果同時有多個值 Put,那麼就會将剩餘的值插入到共享連結清單

poolChain

sync.Pool 使用限制

因為 pool 每次的 get 操作都會将值

remove + return

,相當于用完即抛。并且要注意 Get 的執行過程。Put 方法的參數類型可以是任意類型,一定要切記不要将不同類型的值存進去。如果存在多協程(或循環)調用 Get 時,你無法确定哪次調用的就是你想要的類型而導緻出現未知的錯誤。

文本同步至:https://github.com/MarsonShine/GolangStudy/issues/5

  • 分類 golang 自學系列
  • 标簽 sync.Pool

    , sync.Once

    , sync

    , golang