深入理解 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 核心物件有三個
- New:函式,負責物件初始化
- Get:獲取 Pool 中的物件,如果 Pool 中物件不存在則會呼叫 New
- 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 方法時會做如下方法:
- 查詢
private是否有值,有直接回傳;沒有查詢共享 poolChain 鏈表 - 如果 poolChain 鏈表 pop 回傳的值不為 nil,則直接回傳;如果沒有值則轉向其它 P 中的 poolChain 佇列中存在的值
- 如果其它的 P 的共享佇列中都沒有值,就會嘗試在主存中地址獲取對應的值回傳
- 最終都沒有就會執行 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288342.html
標籤:Go
上一篇:Leetcode No.26 Remove Duplicates from Sorted Array(c++實作)
