深入理解 sync.Once 與 sync.Pool
深入理解 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 時,你無法確定哪次調用的就是你想要的類型而導致出現未知的錯誤。
文本同步至://github.com/MarsonShine/GolangStudy/issues/5