溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Go36-33-臨時(shí)對(duì)象池(sync.Pool)

發(fā)布時(shí)間:2020-09-25 11:11:57 來源:網(wǎng)絡(luò) 閱讀:1998 作者:騎士救兵 欄目:編程語言

臨時(shí)對(duì)象池(sync.Pool)

sync.Pool是Go語言標(biāo)準(zhǔn)庫中的一個(gè)同步工具。

介紹

sync.Pool類型可以被稱為臨時(shí)對(duì)象池,它的值可以被用來存儲(chǔ)臨時(shí)的對(duì)象。它屬于結(jié)構(gòu)體類型,在它的值被真正使用之后,就應(yīng)該再被復(fù)制了。
臨時(shí)對(duì)象,就是不需要持久使用的某一類值。這類值對(duì)于程序來說可有可無,但如果有的話明顯更好。它們的創(chuàng)建和銷毀可以在任何時(shí)候發(fā)生,并且完全不會(huì)影響到程序功能。同時(shí),它們也應(yīng)該是無需被區(qū)分的,其中的任何一個(gè)值都可以代替另一個(gè)。如果某類值完全符合上述條件,就可以把它們存儲(chǔ)到臨時(shí)對(duì)象池中。
可以把臨時(shí)對(duì)象池當(dāng)做針對(duì)某種數(shù)據(jù)的緩存來用,實(shí)際上,這可能就是最主要的用途。連接池好像也能用這個(gè)。

使用方式

sync.Pool類型只有兩個(gè)方法:

  • Put,用于在當(dāng)前的池中存放臨時(shí)對(duì)象,它接受一個(gè)空接口類型的值
  • Get,用于從當(dāng)前的池中獲取臨時(shí)對(duì)象,它返回一個(gè)空接口類型的值

Get方法可能會(huì)中當(dāng)前的池中刪除掉任何一個(gè)值,然后把這個(gè)值作為結(jié)果返回。如果此時(shí)當(dāng)前的池中沒有任何值,那么就會(huì)使用當(dāng)前池的New字段創(chuàng)建一個(gè)新的值,并將其返回。

New字段
sync.Pool類型的New字段是一個(gè)創(chuàng)建臨時(shí)對(duì)象的函數(shù)。它的類型是沒有參數(shù)但是會(huì)返回一個(gè)空接口類型的函數(shù)。即:func() interface{}。
這個(gè)函數(shù)是Get方法最后的獲取到臨時(shí)對(duì)象的手段。函數(shù)的結(jié)果不會(huì)被存入當(dāng)前的臨時(shí)對(duì)象池中,而是直接返回給Get方法的調(diào)用方。
這里的New字段的實(shí)際值需要在初始化臨時(shí)對(duì)象池的時(shí)候就給定。否則,在Get方法調(diào)用它的時(shí)候就會(huì)得到nil。

fmt包中的臨時(shí)對(duì)象

舉個(gè)例子,標(biāo)準(zhǔn)庫的fmt包就用到了sync.Pool類型。fmt包會(huì)創(chuàng)建一個(gè)用于緩存某類臨時(shí)對(duì)象的sync.Pool類型的值,并賦值給ppFree變量。這類臨時(shí)對(duì)象可以識(shí)別格式化和暫存需要打印的內(nèi)容。下面是這部分的源碼:

var ppFree = sync.Pool{
    New: func() interface{} { return new(pp) },
}

臨時(shí)對(duì)象池ppFree的New字段在被調(diào)用的時(shí)候,就是執(zhí)行一個(gè)new方法。new方法是分配內(nèi)存空間,填充零值填充參數(shù)類型,并返回其指針。所以,這里會(huì)返回一個(gè)全新的pp類型值的指針,就是臨時(shí)對(duì)象。這就保證了ppFree的Get方法總能返回一個(gè)包含需要打印內(nèi)容的值。pp類型是fmt包中的私有類型,有很多實(shí)現(xiàn)了不同功能的方法。不過,這里的重點(diǎn)是,它的每一個(gè)值都是獨(dú)立的、平等的和可重用的。
這些對(duì)象既不互相干擾,也不會(huì)受到外部狀態(tài)的影響。由于fmt包真正使用這些零食對(duì)象之前,總是會(huì)先對(duì)其進(jìn)行重置,所以并不在意取到的哪一個(gè)臨時(shí)對(duì)象。這就是臨時(shí)對(duì)象的平等新的具體體現(xiàn)。
另外,這些代碼在使用完臨時(shí)對(duì)象之后,都會(huì)先抹掉其中以緩沖的內(nèi)容,然后再將它存放到ppFree中。這樣就為重用這類臨時(shí)對(duì)象做好了準(zhǔn)備。
打執(zhí)行打印函數(shù),比如:fmt.Println、fmt.Printf等的時(shí)候,都使用了ppFree以及其中的臨時(shí)對(duì)象。因此,在程序同時(shí)執(zhí)行很多的打印函數(shù)調(diào)用的時(shí)候,ppFree可以及時(shí)的提供它緩存的臨時(shí)對(duì)象,這樣就加快了執(zhí)行的速度。
當(dāng)程序在一段時(shí)間內(nèi)不再執(zhí)行打印函數(shù)調(diào)用時(shí),ppFree中的臨時(shí)對(duì)象又能被及時(shí)的清理掉,以節(jié)省內(nèi)存空間。在這個(gè)維度上,臨時(shí)對(duì)象池也可以幫助程序?qū)崿F(xiàn)可伸縮性。這就是它的最大價(jià)值。

自動(dòng)清理機(jī)制

前面將了臨時(shí)對(duì)象會(huì)在什么時(shí)候被創(chuàng)建。這里來講講臨時(shí)對(duì)象會(huì)在什么時(shí)候被銷毀。

池清理函數(shù)
sync包在被初始化的時(shí)候,會(huì)向G系統(tǒng)注冊(cè)一個(gè)函數(shù),這個(gè)函數(shù)的功能就是清楚所有已創(chuàng)建的臨時(shí)對(duì)象池中的值??梢园堰@個(gè)函數(shù)稱為池清理函數(shù)。注冊(cè)之后,在每次即將執(zhí)行垃圾回收時(shí)都會(huì)執(zhí)行池清理函數(shù)。

池匯總列表
另外,在sync包中還有一個(gè)包級(jí)私有的全局變量。這個(gè)變量記錄了程序中使用的所有臨時(shí)對(duì)象池的匯總,它是元素類型為*sync.Pool的切片??梢苑Q之為池匯總列表。通常,在一個(gè)臨時(shí)對(duì)象池的Put方法或Get方法第一次被調(diào)用的時(shí)候,這個(gè)池就會(huì)被添加到池匯總列表中。這樣,池清理函數(shù)總是能訪問到所有正在被真正使用的臨時(shí)對(duì)象池。

清理的過程
Go語言運(yùn)行時(shí)系統(tǒng)中的垃圾回收器會(huì)在每次開始執(zhí)行前先執(zhí)行池清理函數(shù)。
池清理函數(shù)會(huì)遍歷池匯總列表,對(duì)其中的每一個(gè)臨時(shí)對(duì)象池,它都會(huì)先將池中所有的私有臨時(shí)對(duì)象共享臨時(shí)對(duì)象列表都置為nil,然后再把這個(gè)池中的所有本地池列表都銷毀掉。
然后,池清理函數(shù)會(huì)把池匯總列表重置為空的切片。這樣,這些池中存儲(chǔ)的臨時(shí)對(duì)象就全部被清除干凈了。
最后,就是垃圾回收器了。如果臨時(shí)對(duì)象池以外的代碼再無對(duì)它們的引用,在稍后的垃圾回收過程中,這些臨時(shí)對(duì)象就會(huì)被當(dāng)做垃圾銷毀掉,它們占用的內(nèi)存空間也會(huì)被回收。

小結(jié)
上面的清理過程的說明中,有幾個(gè)重點(diǎn)標(biāo)出的詞:私有臨時(shí)對(duì)象、共享臨時(shí)對(duì)象列表和本地池列表。這些會(huì)在下面展開。

臨時(shí)對(duì)象池存儲(chǔ)值的數(shù)據(jù)結(jié)構(gòu)

在臨時(shí)對(duì)象池中,有一個(gè)多層的數(shù)據(jù)結(jié)構(gòu)。
這個(gè)數(shù)據(jù)結(jié)構(gòu)的頂層,是本地池列表,它是一個(gè)數(shù)組。列表的長度總是與Go語言調(diào)度器中的P的數(shù)量相同。

這里提到了P,引申開來,再次說明一下G-P-M模型:

在Go語言調(diào)度器中的P是processor的縮寫,它指的是一種可以承載若干個(gè)G、并且能夠使這些G適時(shí)地與M進(jìn)行對(duì)接,并得到真正運(yùn)行的中介。
這里的G正式goroutine的縮寫,而M則是machine的縮寫,M是系統(tǒng)級(jí)的線程。正式因?yàn)镻的存在,G和M才能夠進(jìn)行靈活、高效的配對(duì),從而實(shí)現(xiàn)強(qiáng)大的并發(fā)編程模型。

P存在的一個(gè)很重要的原因是為了分散并發(fā)程序的執(zhí)行壓力,而讓臨時(shí)對(duì)象池中的本地池列表的長度與P的數(shù)量相同的主要原因也是分散壓力。這里的壓力包括存儲(chǔ)和性能兩個(gè)方面。
回到數(shù)據(jù)結(jié)構(gòu),在本地池列表中的每個(gè)本地池都包含了3個(gè)字段,或者說組件:

  • private : 存儲(chǔ)私有臨時(shí)對(duì)象的字段,空接口類型,存一個(gè)值
  • shared : 共享臨時(shí)對(duì)象列表的字段,空接口的切片,存一組值
  • sync.Mutext : 嵌入式鎖(Embedded lock),保護(hù)shared字段

具體的數(shù)據(jù)結(jié)構(gòu)在源碼中如下:

// Local per-P Pool appendix.
type poolLocalInternal struct {
    private interface{}   // Can be used only by the respective P.
    shared  []interface{} // Can be used by any P.
    Mutex                 // Protects shared.
}

每個(gè)本地池都對(duì)應(yīng)著一個(gè)P。一個(gè)goroutine想要真正運(yùn)行就必須先與某個(gè)P產(chǎn)生關(guān)聯(lián),所以一個(gè)正在運(yùn)行的goroutine必然會(huì)關(guān)聯(lián)著某個(gè)P。在程序調(diào)用臨時(shí)對(duì)象池的Put方法或Get方法的時(shí)候,總會(huì)先試圖從臨時(shí)對(duì)象池的本地池列表中獲取對(duì)應(yīng)的本地池,依據(jù)的就是與當(dāng)前goroutine關(guān)聯(lián)的那個(gè)P的ID。就是說,有一個(gè)goroutine,就會(huì)有一個(gè)與之一一對(duì)應(yīng)的本地池,一個(gè)臨時(shí)對(duì)象池的Put方法或Get方法會(huì)根據(jù)它所在的goroutine關(guān)聯(lián)到P,然后獲取到那個(gè)對(duì)應(yīng)的本地池。

臨時(shí)對(duì)象池存取值的過程

Put方法,會(huì)先試圖把新的臨時(shí)對(duì)象存儲(chǔ)到本地池的private字段中。這樣在需要獲取臨時(shí)對(duì)象的時(shí)候,可以快速的拿到一個(gè)可用的值。只有當(dāng)private字段已經(jīng)存有某個(gè)值的時(shí)候,Put方法才會(huì)去訪問本地池的shared字段進(jìn)行存儲(chǔ)。
Get方法,會(huì)先試圖從本地池private字段出獲取一個(gè)臨時(shí)對(duì)象。只有當(dāng)private字段的值為nil時(shí),才會(huì)去訪問本地池的shared字段獲取對(duì)象。
shared字段,原則上可以被任何goroutine中的代碼訪問到,字段類型是切片,可以存放一組臨時(shí)對(duì)象。
private字段,只可能被與之對(duì)應(yīng)的P所關(guān)聯(lián)的goroutine中的代碼訪問到,可以說它是P級(jí)私有的。字段里只能存放一個(gè)臨時(shí)對(duì)象。
在訪問本地池的shared字段時(shí),由于shared字段是共享的,必須受到互斥鎖的保護(hù),這個(gè)鎖正是在結(jié)構(gòu)體中嵌入的Mutex。而訪問本地池的private字段是,不需要保護(hù),所以這個(gè)private字段的存在是為了提供運(yùn)行效率的。
在回到Put方法個(gè)Get方法,Get方法只需要去訪問與之對(duì)應(yīng)的本地池,先試著往private里存,如果已經(jīng)有了就存到shared里。而Get方法,在訪問過對(duì)應(yīng)的本地池的private和shared之后仍沒有獲取到任何對(duì)象,那么就會(huì)去訪問臨時(shí)對(duì)象池中的所有本地池,這些本地池都在本地池列表里。由于不是與之對(duì)應(yīng)了本地池了,只能訪問shared字段,嘗試獲取到一個(gè)對(duì)象。這一步也是可能無法獲取到一個(gè)可用的臨時(shí)對(duì)象的,可能是都被取走了,也可能是剛被大清洗過。還沒有的話,就是最后一個(gè)手段了,調(diào)用對(duì)象池(sync.Pool)里New字段的函數(shù)創(chuàng)建一個(gè)新的臨時(shí)對(duì)象。另外New字段是需要在初始化對(duì)象池的時(shí)候給定的,否則會(huì)返回nil,這樣的話Get方法也只能返回nil了。

示例

本篇主要是一些概念,臨時(shí)對(duì)象池的使用起來還是比較簡單的。
下面的例子中,關(guān)于臨時(shí)對(duì)象池使用的代碼并不多,本身也并不需要多少。代碼中花了很大的精力在構(gòu)造一個(gè)讀寫緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu),真正需要使用到臨時(shí)對(duì)象的時(shí)候,往往有現(xiàn)成的對(duì)象可用,或是已經(jīng)在別處定義好對(duì)象了。不過作為一個(gè)demo還是很完整的,值得參考:

package main

import (
    "io"
    "bytes"
    "fmt"
    "sync"
)

// 存放數(shù)據(jù)塊緩沖區(qū)的臨時(shí)對(duì)象
var bufPool sync.Pool

// 預(yù)定義定界符
const delimiter = '\n'

// 一個(gè)簡易的數(shù)據(jù)庫緩沖區(qū)的接口
type Buffer interface {
    Delimiter() byte                    // 獲取數(shù)據(jù)塊之間的定界符
    Write(contents string) (err error)  // 寫入一個(gè)數(shù)據(jù)塊
    Read() (contents string, err error) // 讀取一個(gè)數(shù)據(jù)塊
    Free()                              // 釋放當(dāng)前的緩沖區(qū)
}

// 實(shí)現(xiàn)一個(gè)上面定義的接口
type myBuffer struct {
    buf       bytes.Buffer
    delimiter byte
}

func (b *myBuffer) Delimiter() byte {
    return b.delimiter
}

func (b *myBuffer) Write (contents string) (err error) {
    if _, err = b.buf.WriteString(contents); err != nil {
        return
    }
    return b.buf.WriteByte(b.delimiter)
}

func (b *myBuffer) Read() (contents string, err error) {
    return b.buf.ReadString(b.delimiter)
}

func (b *myBuffer) Free() {
    bufPool.Put(b)
}

func init() {
    bufPool = sync.Pool{
        New: func() interface{} {
            return &myBuffer{delimiter: delimiter}
        },
    }
}

// 獲取一個(gè)數(shù)據(jù)庫緩沖區(qū)
func GetBuffer() Buffer {
    return bufPool.Get().(Buffer)
}

func main() {
    buf := GetBuffer()
    defer buf.Free()
    buf.Write("寫入第一行,")
    buf.Write("接著寫第二行。")
    fmt.Println("數(shù)據(jù)已經(jīng)寫入,準(zhǔn)備把數(shù)據(jù)讀出")
    for {
        block, err := buf.Read()
        if err != nil {
            if err == io.EOF {
                break
            }
            panic(fmt.Errorf("讀取緩沖區(qū)時(shí)ERROR: %s", err))
        }
        fmt.Print(block)
    }
}

總結(jié)

sync.Pool類型是一個(gè)比較有用的同步工具,它的值被稱為臨時(shí)對(duì)象池。
臨時(shí)對(duì)象池有一個(gè)New字段,在初始化的時(shí)候最好給定它,是一個(gè)用來創(chuàng)建臨時(shí)對(duì)象的函數(shù)。臨時(shí)對(duì)象池還有兩個(gè)方法:Put和Get,分別用于向池中存放和獲取臨時(shí)對(duì)象。
還分析了臨時(shí)對(duì)象池內(nèi)部存儲(chǔ)臨時(shí)對(duì)象值的數(shù)據(jù)結(jié)構(gòu),正是因?yàn)橛羞@樣的一個(gè)數(shù)據(jù)結(jié)構(gòu)的支撐,臨時(shí)對(duì)象池才能夠有效的分散存儲(chǔ)壓力性能壓力。通過分析臨時(shí)對(duì)象池存取值的過程,了解到Get方法對(duì)這個(gè)數(shù)據(jù)結(jié)構(gòu)的妙用,使得其中的臨時(shí)對(duì)象可以被高效的利用。
這樣的內(nèi)部結(jié)構(gòu)和存取方式,讓臨時(shí)對(duì)象池成為了一個(gè)特點(diǎn)鮮明的同步工具。它存儲(chǔ)的臨時(shí)對(duì)象都應(yīng)該是擁有較長生命周期的值,并且這些值不應(yīng)該被某個(gè)goroutine中的代碼長期持有和使用。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI