首頁 > 軟體

深入淺出Golang中的sync.Pool

2023-11-03 10:00:29

學習到的內容:

1.一個64位元的int型別值,充分利用高32位元和低32位元,進行相關加減以及從一個64位元中拆出高32位元和低32位元.

擴充套件:如何自己實現一個無鎖佇列.

  • 如何判斷佇列是否滿.
  • 如何實現無鎖化.
  • 優化方面需要思考的東西.

2.記憶體相關操作以及優化

  • 記憶體對齊
  • CPU Cache Line
  • 直接操作記憶體.

一、原理分析

1.1 結構依賴關係圖

下面是相關原始碼,不過是已經刪減了對本次分析沒有用的程式碼.

type Pool struct {
    // GMP中,每一個P(協程排程器)會有一個陣列,陣列大小位localSize. 
 local     unsafe.Pointer 
 // p 陣列大小.
 localSize uintptr
 New func() any
}

// poolLocal 每個P(協程排程器)的本地pool.
type poolLocal struct {
 poolLocalInternal
    // 保證一個poolLocal佔用一個快取行
 pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

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

type poolChain struct {
 head *poolChainElt
 tail *poolChainElt
}

type poolChainElt struct {
 poolDequeue
 next, prev *poolChainElt
}

type poolDequeue struct {
 // head 高32位元,tail低32位元.
 headTail uint64
 vals []eface
}

// 儲存具體的value. 
type eface struct {
 typ, val unsafe.Pointer
}

1.2 用圖讓程式碼說話

1.3 Put過程分析

Put 過程分析比較重要,因為這裡會包含pool所有依賴相關分析.

總的分析學習過程可以分為下面幾個步驟:

1.獲取P對應的poolLocal

2.val如何進入poolLocal下面的poolDequeue佇列中的.

3.如果當前協程獲取到當前P對應的poolLocal之後進行put前,協程讓出CPU使用權,再次排程過來之後,會發生什麼?

4.讀寫記憶體優化.

陣列直接操作記憶體,而不經過Golang

充分利用uint64值的特性,將headtail用一個值來進行表示,減少CPU存取記憶體次數.

獲取P對應的poolLocal

sync.Pool.local其實是一個指標,並且通過變數+結構體大小來劃分記憶體空間,從而將這片記憶體直接劃分為陣列. Go 在Put之前會先對當前Goroutine繫結到當前P中,然後通過pid獲取其在local記憶體地址中的歧視指標,在獲取時是會進行記憶體分配的. 具體如下:

func (p *Pool) pin() (*poolLocal, int) {
 // 返回執行當前協程的P(協程排程器),並且設定禁止搶佔.
 pid := runtime_procPin()
 s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
 l := p.local                              // load-consume
 // pid < 核心數. 預設走該邏輯.
 if uintptr(pid) < s {
  return indexLocal(l, pid), pid
 }
 // 設定的P大於本機CPU核心數.
 return p.pinSlow()
}

// indexLocal 獲取當前P的poolLocal指標. 
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
 // l p.local指標開始位置.
 // 我猜測這裡如果l為空,編譯階段會進行優化. 
 lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
 // uintptr真實的指標.
 // unsafe.Pointer Go對指標的封裝: 用於指標和結構體互相轉化.
 return (*poolLocal)(lp)
}

從上面程式碼我們可以看到,Go通過runtime_procPin來設定當前Goroutine獨佔P,並且直接通過頭指標+偏移量(陣列結構體大小)來進行對記憶體劃分為陣列.

Put 進入poolDequeue佇列:

Go在Push時,會通過headtail來獲取當前佇列內元素個數,如果滿了,則會重新構建一個環型佇列poolChainElt,並且設定為poolChain.head,並且賦值next以及prev.

通過下面程式碼,我們可以看到,Go通過邏輯運算判斷佇列是否滿的設計時非常巧妙的,如果後續我們去開發元件,也是可以這麼進行設計的。

func (c *poolChain) pushHead(val any) {
 d := c.head
    // 初始化. 
 if d == nil {
  // Initialize the chain.
  const initSize = 8 // Must be a power of 2
  d = new(poolChainElt)
  d.vals = make([]eface, initSize)
  c.head = d
  // 將新構建的d賦值給tail.
  storePoolChainElt(&c.tail, d)
 }
 // 入隊.
 if d.pushHead(val) {
  return
 }
 // 佇列滿了.
 newSize := len(d.vals) * 2
 if newSize >= dequeueLimit {
        // 佇列大小預設為2的30次方. 
  newSize = dequeueLimit
 }

    // 賦值連結串列前後節點關係. 
 // prev.
 // d2.prev=d1.
 // d1.next=d2.
 d2 := &poolChainElt{prev: d}
 d2.vals = make([]eface, newSize)
 c.head = d2
 // next .
 storePoolChainElt(&d.next, d2)
 d2.pushHead(val)
}

// 入隊poolDequeue
func (d *poolDequeue) pushHead(val any) bool {
 ptrs := atomic.LoadUint64(&d.headTail)
 head, tail := d.unpack(ptrs)
 // head 表示當前有多少元素.
 if (tail+uint32(len(d.vals)))&(1<<dequeueBits-1) == head {
  return false
 }
 // 環型佇列. head&uint32(len(d.vals)-1) 表示當前元素落的位置一定在佇列上.
 slot := &d.vals[head&uint32(len(d.vals)-1)]

 typ := atomic.LoadPointer(&slot.typ)
 if typ != nil {
  return false
 }

 // The head slot is free, so we own it.
 if val == nil {
  val = dequeueNil(nil)
 }
    // 向slot寫入指標型別為*any,並且值為val.
 *(*any)(unsafe.Pointer(slot)) = val
    // headTail高32位元++
 atomic.AddUint64(&d.headTail, 1<<dequeueBits)
 return true
}

Get實現邏輯:

其實我們看了Put相關邏輯之後,我們可能很自然的就想到了Get的邏輯,無非就是遍歷連結串列,並且如果佇列中最後一個元素不為空,則會將該元素返回,並且將該插槽賦值為空值.

二、學習收穫

如何自己實現一個無鎖佇列. 本文未實現,後續文章會進行實現.

2.1 如何自己實現一個無鎖佇列

橫向思考,並未進行實現,後續會進行實現“

  • 儲存直接使用指標來進行儲存,充分利用uintptrunsafe.Pointer和結構體指標之間的依賴關係來提升效能.
  • 狀態儲存要考慮CPU Cache Line、記憶體對齊以及減少存取記憶體次數等相關問題.
  • 充分利用Go中的原子操作包來進行實現,通過atomic.CompareAndSwapPointer來設計自旋來達到無鎖化.

到此這篇關於深入淺出Golang中的sync.Pool的文章就介紹到這了,更多相關Golang sync.Pool內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com