<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 并發(fā)編程 — 深入淺出 sync.Pool ,圍觀最全的使用姿勢(shì),理解最深刻的原理

          共 6955字,需瀏覽 14分鐘

           ·

          2021-02-28 20:36


          大綱


          • 使用姿勢(shì)

            • 初始化 Pool 實(shí)例 New

            • 申請(qǐng)對(duì)象 Get

            • 釋放對(duì)象 Put

          • 思考

            • 為什么用 Pool,而不是在運(yùn)行的時(shí)候直接實(shí)例化對(duì)象呢?

            • `sync.Pool` 是并發(fā)安全的嗎?

            • 為什么 `sync.Pool` 不適合用于像 socket 長(zhǎng)連接或數(shù)據(jù)庫(kù)連接池?

          • 總結(jié)


          概要


          Go 并發(fā)相關(guān)庫(kù) sync 里面有一個(gè)有趣的 package Pool,sync.Pool 是個(gè)有趣的庫(kù),用很少的代碼實(shí)現(xiàn)了很巧的功能。第一眼看到 Pool 這個(gè)名字,就讓人想到池子,元素池化是常用的性能優(yōu)化的手段(性能優(yōu)化的幾把斧頭:并發(fā),預(yù)處理,緩存)。比如,創(chuàng)建一個(gè) 100 個(gè)元素的池,然后就可以在池子里面直接獲取到元素,免去了申請(qǐng)和初始化的流程,大大提高了性能。釋放元素也是直接丟回池子而免去了真正釋放元素帶來(lái)的開(kāi)銷(xiāo)。

          但是再仔細(xì)一看 sync.Pool 的實(shí)現(xiàn),發(fā)現(xiàn)比我預(yù)期的還更有趣。sync.Pool 除了最常見(jiàn)的池化提升性能的思路,最重要的是減少 GC 。常用于一些對(duì)象實(shí)例創(chuàng)建昂貴的場(chǎng)景。注意,Pool 是 Goroutine 并發(fā)安全的。



          使用姿勢(shì)


          初始化 Pool 實(shí)例 New

          第一個(gè)步驟就是創(chuàng)建一個(gè) Pool 實(shí)例,關(guān)鍵一點(diǎn)是配置 New 方法,聲明 Pool 元素創(chuàng)建的方法。

          bufferpool := &sync.Pool {
              New: func() interface {} {
                  println("Create new instance")
                  return struct{}{}
              }
          }


          申請(qǐng)對(duì)象 Get

          buffer := bufferPool.Get()

          Get 方法會(huì)返回 Pool 已經(jīng)存在的對(duì)象,如果沒(méi)有,那么就走慢路徑,也就是調(diào)用初始化的時(shí)候定義的 New 方法(也就是最開(kāi)始定義的初始化行為)來(lái)初始化一個(gè)對(duì)象。


          釋放對(duì)象 Put

          bufferPool.Put(buffer)

          使用對(duì)象之后,調(diào)用 Put 方法聲明把對(duì)象放回池子。注意了,這個(gè)調(diào)用之后僅僅是把這個(gè)對(duì)象放回池子,池子里面的對(duì)象啥時(shí)候真正釋放外界是不清楚的,是不受外部控制的。

          你看,Pool 的用戶(hù)使用界面就這三個(gè)接口,非常簡(jiǎn)單,而且是通用型的 Pool 池模式,針對(duì)所有的對(duì)象類(lèi)型都可以用。


          思考


          為什么用 Pool,而不是在運(yùn)行的時(shí)候直接實(shí)例化對(duì)象呢?

          本質(zhì)原因:Go 的內(nèi)存釋放是由 runtime 來(lái)自動(dòng)處理的,有 GC 過(guò)程。

          舉個(gè)栗子

          package main

          import (
              "fmt"
              "sync"
              "sync/atomic"
          )

          // 用來(lái)統(tǒng)計(jì)實(shí)例真正創(chuàng)建的次數(shù)
          var numCalcsCreated int32

          // 創(chuàng)建實(shí)例的函數(shù)
          func createBuffer() interface{} {
              // 這里要注意下,非常重要的一點(diǎn)。這里必須使用原子加,不然有并發(fā)問(wèn)題;
              atomic.AddInt32(&numCalcsCreated, 1)
              buffer := make([]byte1024)
              return &buffer
          }

          func main() {
              // 創(chuàng)建實(shí)例
              bufferPool := &sync.Pool{
                  New: createBuffer,
              }

              // 多 goroutine 并發(fā)測(cè)試
              numWorkers := 1024 * 1024
              var wg sync.WaitGroup
              wg.Add(numWorkers)

              for i := 0; i < numWorkers; i++ {
                  go func() {
                      defer wg.Done()
                      // 申請(qǐng)一個(gè) buffer 實(shí)例
                      buffer := bufferPool.Get()
                      _ = buffer.(*[]byte)
                      // 釋放一個(gè) buffer 實(shí)例
                      defer bufferPool.Put(buffer)
                  }()
              }
              wg.Wait()
              fmt.Printf("%d buffer objects were created.\n", numCalcsCreated)
          }

          上面的例子可以直接復(fù)制運(yùn)行起來(lái)看下,控制臺(tái)輸出:

          ?  pool# go run test_pool.go        
          3 buffer objects were created.
          ?  pool# go run test_pool.go
          4 buffer objects were created.

          程序 go run 運(yùn)行了兩次,一次結(jié)果是 3 ,一次是 4 。這個(gè)是什么原因呢?

          首先,這個(gè)是正常的情況,不知道你有沒(méi)有注意到,創(chuàng)建 Pool 實(shí)例的時(shí)候,只要求填充了 New 函數(shù),而根本沒(méi)有聲明或者限制這個(gè) Pool 的大小。所以,記住一點(diǎn),程序員作為使用方不能對(duì) Pool 里面的元素個(gè)數(shù)做假定。

          再來(lái),如果我不用 Pool 來(lái)申請(qǐng)實(shí)例,而是直接申請(qǐng),也就是上面的代碼只改一行:

          將以下代碼:

          // 申請(qǐng)一個(gè) buffer 實(shí)例
          buffer := bufferPool.Get()

          修改成:

          // 申請(qǐng)一個(gè) buffer 實(shí)例
          buffer := createBuffer()

          這個(gè)時(shí)候,我們?cè)賵?zhí)行程序 go run test_pool.go,會(huì)發(fā)現(xiàn)什么?

          ?  pool go run test_pool_1.go
          1048576 buffer objects were created.
          ?  pool go run test_pool_1.go
          1048576 buffer objects were created.

          注意到,和之前有兩個(gè)不同點(diǎn)

          1. 同樣也是運(yùn)行兩次,兩次結(jié)果相同。
          2. 對(duì)象創(chuàng)建的數(shù)量和并發(fā) Worker 數(shù)量相同,數(shù)量等于 1048576 (這個(gè)就是 1024*1024);

          原因很簡(jiǎn)單,因?yàn)槊看味际侵苯诱{(diào)用 createBuffer 函數(shù)申請(qǐng) buffer,有 1048576 個(gè)并發(fā) Worker 調(diào)用,所以跑多少次結(jié)果都會(huì)是 1048576。

          實(shí)際上還有一個(gè)不同點(diǎn),就是程序跑的過(guò)程中,該進(jìn)程分配消耗的內(nèi)存很大。因?yàn)?Go 申請(qǐng)內(nèi)存是程序員觸發(fā)的,回收卻是 Go 內(nèi)部 runtime GC 回收器來(lái)執(zhí)行的,這是一個(gè)異步的操作。這種業(yè)務(wù)不負(fù)責(zé)任的內(nèi)存使用會(huì)對(duì) GC 帶來(lái)非常大的負(fù)擔(dān),進(jìn)而影響整體程序的性能。

          類(lèi)比現(xiàn)實(shí)的例子

          一個(gè)程序猿喝奶茶,需要一個(gè)吸管(吸管類(lèi)比就是我們代碼里的 buffer 對(duì)象嘍),奶茶喝完吸管就扔了,那就是塑料垃圾了( Garbage )。清潔工老李( GC 回收器 )需要緊跟在后面打掃衛(wèi)生,現(xiàn)在 1048576 個(gè)程序猿同時(shí)喝奶茶,每個(gè)人都現(xiàn)場(chǎng)要一根新吸管,喝完就扔,馬上地上有 1048576 個(gè)塑料吸管垃圾。清潔工老李估計(jì)要累個(gè)半死。

          那如果,現(xiàn)在在某個(gè)隱秘的角落放一個(gè)回收箱 ( 類(lèi)比成 sync.Pool ) ,程序員喝完奶茶之后,吸管就丟到回收箱里,下一個(gè)程序員要用吸管的話(huà),伸手進(jìn)箱子摸一下,看下有管子嗎?有的話(huà),就拿來(lái)用了。沒(méi)有的話(huà),就再找人要一根新吸管。這樣新吸管的使用數(shù)量就大大減少了呀,地上也沒(méi)垃圾了,老李也輕松了,多好呀。

          并且,極限情況下,如果大家喝奶茶足夠快,保證箱子里每時(shí)每刻都至少有一根用過(guò)的吸管,那 1048576 個(gè)程序員估計(jì)用一根吸管都?jí)蛄恕?。。。(有點(diǎn)想吐。。。)

          回歸正題

          這就也解釋了,為什么使用 sync.Pool 之后數(shù)量只有 3,4 個(gè)。但是進(jìn)一步思考:為什么 sync.Pool 的兩次使用結(jié)果輸出不不一樣呢?

          因?yàn)閺?fù)用的速度不一樣。我們不能對(duì) Pool 池里的 cache 的元素個(gè)數(shù)做任何假設(shè)。不過(guò)還是那句話(huà),如果速度足夠快,其實(shí)里面可以只有一個(gè)元素就可以服務(wù) 1048576 個(gè)并發(fā)的 Goroutine 。


          sync.Pool 是并發(fā)安全的嗎?


          sync.Pool 當(dāng)然是并發(fā)安全的。官方文檔里明確說(shuō)了:

          A Pool is safe for use by multiple goroutines simultaneously.

          但是,為什么我這里會(huì)單獨(dú)提出來(lái)呢?

          因?yàn)?sync.Pool 只是本身的 Pool 數(shù)據(jù)結(jié)構(gòu)是并發(fā)安全的,并不是說(shuō) Pool.New 函數(shù)一定是線(xiàn)程安全的。Pool.New 函數(shù)可能會(huì)被并發(fā)調(diào)用 ,如果 New 函數(shù)里面的實(shí)現(xiàn)是非并發(fā)安全的,那就會(huì)有問(wèn)題。

          細(xì)心的小伙伴會(huì)注意到我在上面的代碼例子里,關(guān)于 createBuffer 函數(shù)的實(shí)現(xiàn)里,對(duì)于 numCalcsCreated 的計(jì)數(shù)加是用原子操作的:atomic.AddInt32(&numCalcsCreated, 1) 。

          func createBuffer() interface{} {
              // 這里要注意下,非常重要的一點(diǎn)。這里必須使用原子加,不然有并發(fā)問(wèn)題;
              atomic.AddInt32(&numCalcsCreated, 1)
              buffer := make([]byte1024)
              return &buffer
          }

          因?yàn)?numCalcsCreated 是個(gè)全局變量,Pool.New( 也就是 createBuffer ) 并發(fā)調(diào)用的時(shí)候,會(huì)導(dǎo)致 data race ,所以只有用原子操作才能保證數(shù)據(jù)的正確性。

          小伙伴們可以嘗試下,把 atomic.AddInt32(&numCalcsCreated, 1) 這樣代碼改成 numCalcsCreated++ ,然后用 go run -race test_pool.go 命令檢查一下,肯定會(huì)報(bào)告告警的,類(lèi)似如下:

          WARNING: DATA RACE
          Read at 0x000001287538 by goroutine 10:

          Previous write at 0x000001287538 by goroutine 7:

          ==================
          ==================
          WARNING: DATA RACE
          Read at 0x000001287538 by goroutine 9:
            main.createBuffer()

          本質(zhì)原因:Pool.New 函數(shù)可能會(huì)被并發(fā)調(diào)用。


          為什么 sync.Pool 不適合用于像 socket 長(zhǎng)連接或數(shù)據(jù)庫(kù)連接池?


          因?yàn)?,我們不能?duì) sync.Pool 中保存的元素做任何假設(shè),以下事情是都可以發(fā)生的:

          1. Pool 池里的元素隨時(shí)可能釋放掉,釋放策略完全由 runtime 內(nèi)部管理;
          2. Get 獲取到的元素對(duì)象可能是剛創(chuàng)建的,也可能是之前創(chuàng)建好 cache 住的。使用者無(wú)法區(qū)分;
          3. Pool 池里面的元素個(gè)數(shù)你無(wú)法知道;

          所以,只有的你的場(chǎng)景滿(mǎn)足以上的假定,才能正確的使用 Pool 。sync.Pool 本質(zhì)用途是增加臨時(shí)對(duì)象的重用率,減少 GC 負(fù)擔(dān)。劃重點(diǎn):臨時(shí)對(duì)象。所以說(shuō),像 socket 這種帶狀態(tài)的,長(zhǎng)期有效的資源是不適合 Pool 的。


          總結(jié)


          1. sync.Pool 本質(zhì)用途是增加臨時(shí)對(duì)象的重用率,減少 GC 負(fù)擔(dān);
          2. 不能對(duì) Pool.Get 出來(lái)的對(duì)象做預(yù)判,有可能是新的(新分配的),有可能是舊的(之前人用過(guò),然后 Put 進(jìn)去的);
          3. 不能對(duì) Pool 池里的元素個(gè)數(shù)做假定,你不能夠;
          4. sync.Pool 本身的 Get, Put 調(diào)用是并發(fā)安全的,sync.New 指向的初始化函數(shù)會(huì)并發(fā)調(diào)用,里面安不安全只有自己知道;
          5. 當(dāng)用完一個(gè)從 Pool 取出的實(shí)例時(shí)候,一定要記得調(diào)用 Put,否則 Pool 無(wú)法復(fù)用這個(gè)實(shí)例,通常這個(gè)用 defer 完成;


          推薦閱讀


          福利

          我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 15
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  a欧美| 成人国产经典视频 | 免费观看黄色成人网站 | 中文字幕 亚洲 日本 欧美 | x8x8拨牐拨牐精品视频 |