sync.Pool是Go語言標準庫中的一個同步工具。
創(chuàng)新互聯(lián)是一家專業(yè)提供安義企業(yè)網(wǎng)站建設(shè),專注與成都網(wǎng)站建設(shè)、做網(wǎng)站、H5頁面制作、小程序制作等業(yè)務(wù)。10年已為安義眾多企業(yè)、政府機構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)站建設(shè)公司優(yōu)惠進行中。sync.Pool類型可以被稱為臨時對象池,它的值可以被用來存儲臨時的對象。它屬于結(jié)構(gòu)體類型,在它的值被真正使用之后,就應(yīng)該再被復制了。
臨時對象,就是不需要持久使用的某一類值。這類值對于程序來說可有可無,但如果有的話明顯更好。它們的創(chuàng)建和銷毀可以在任何時候發(fā)生,并且完全不會影響到程序功能。同時,它們也應(yīng)該是無需被區(qū)分的,其中的任何一個值都可以代替另一個。如果某類值完全符合上述條件,就可以把它們存儲到臨時對象池中。
可以把臨時對象池當做針對某種數(shù)據(jù)的緩存來用,實際上,這可能就是最主要的用途。連接池好像也能用這個。
sync.Pool類型只有兩個方法:
Get方法可能會中當前的池中刪除掉任何一個值,然后把這個值作為結(jié)果返回。如果此時當前的池中沒有任何值,那么就會使用當前池的New字段創(chuàng)建一個新的值,并將其返回。
New字段
sync.Pool類型的New字段是一個創(chuàng)建臨時對象的函數(shù)。它的類型是沒有參數(shù)但是會返回一個空接口類型的函數(shù)。即:func() interface{}
。
這個函數(shù)是Get方法最后的獲取到臨時對象的手段。函數(shù)的結(jié)果不會被存入當前的臨時對象池中,而是直接返回給Get方法的調(diào)用方。
這里的New字段的實際值需要在初始化臨時對象池的時候就給定。否則,在Get方法調(diào)用它的時候就會得到nil。
舉個例子,標準庫的fmt包就用到了sync.Pool類型。fmt包會創(chuàng)建一個用于緩存某類臨時對象的sync.Pool類型的值,并賦值給ppFree變量。這類臨時對象可以識別格式化和暫存需要打印的內(nèi)容。下面是這部分的源碼:
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
臨時對象池ppFree的New字段在被調(diào)用的時候,就是執(zhí)行一個new方法。new方法是分配內(nèi)存空間,填充零值填充參數(shù)類型,并返回其指針。所以,這里會返回一個全新的pp類型值的指針,就是臨時對象。這就保證了ppFree的Get方法總能返回一個包含需要打印內(nèi)容的值。pp類型是fmt包中的私有類型,有很多實現(xiàn)了不同功能的方法。不過,這里的重點是,它的每一個值都是獨立的、平等的和可重用的。
這些對象既不互相干擾,也不會受到外部狀態(tài)的影響。由于fmt包真正使用這些零食對象之前,總是會先對其進行重置,所以并不在意取到的哪一個臨時對象。這就是臨時對象的平等新的具體體現(xiàn)。
另外,這些代碼在使用完臨時對象之后,都會先抹掉其中以緩沖的內(nèi)容,然后再將它存放到ppFree中。這樣就為重用這類臨時對象做好了準備。
打執(zhí)行打印函數(shù),比如:fmt.Println、fmt.Printf等的時候,都使用了ppFree以及其中的臨時對象。因此,在程序同時執(zhí)行很多的打印函數(shù)調(diào)用的時候,ppFree可以及時的提供它緩存的臨時對象,這樣就加快了執(zhí)行的速度。
當程序在一段時間內(nèi)不再執(zhí)行打印函數(shù)調(diào)用時,ppFree中的臨時對象又能被及時的清理掉,以節(jié)省內(nèi)存空間。在這個維度上,臨時對象池也可以幫助程序?qū)崿F(xiàn)可伸縮性。這就是它的大價值。
前面將了臨時對象會在什么時候被創(chuàng)建。這里來講講臨時對象會在什么時候被銷毀。
池清理函數(shù)
sync包在被初始化的時候,會向G系統(tǒng)注冊一個函數(shù),這個函數(shù)的功能就是清楚所有已創(chuàng)建的臨時對象池中的值??梢园堰@個函數(shù)稱為池清理函數(shù)。注冊之后,在每次即將執(zhí)行垃圾回收時都會執(zhí)行池清理函數(shù)。
池匯總列表
另外,在sync包中還有一個包級私有的全局變量。這個變量記錄了程序中使用的所有臨時對象池的匯總,它是元素類型為*sync.Pool的切片。可以稱之為池匯總列表。通常,在一個臨時對象池的Put方法或Get方法第一次被調(diào)用的時候,這個池就會被添加到池匯總列表中。這樣,池清理函數(shù)總是能訪問到所有正在被真正使用的臨時對象池。
清理的過程
Go語言運行時系統(tǒng)中的垃圾回收器會在每次開始執(zhí)行前先執(zhí)行池清理函數(shù)。
池清理函數(shù)會遍歷池匯總列表,對其中的每一個臨時對象池,它都會先將池中所有的私有臨時對象和共享臨時對象列表都置為nil,然后再把這個池中的所有本地池列表都銷毀掉。
然后,池清理函數(shù)會把池匯總列表重置為空的切片。這樣,這些池中存儲的臨時對象就全部被清除干凈了。
最后,就是垃圾回收器了。如果臨時對象池以外的代碼再無對它們的引用,在稍后的垃圾回收過程中,這些臨時對象就會被當做垃圾銷毀掉,它們占用的內(nèi)存空間也會被回收。
小結(jié)
上面的清理過程的說明中,有幾個重點標出的詞:私有臨時對象、共享臨時對象列表和本地池列表。這些會在下面展開。
在臨時對象池中,有一個多層的數(shù)據(jù)結(jié)構(gòu)。
這個數(shù)據(jù)結(jié)構(gòu)的頂層,是本地池列表,它是一個數(shù)組。列表的長度總是與Go語言調(diào)度器中的P的數(shù)量相同。
這里提到了P,引申開來,再次說明一下G-P-M模型:
在Go語言調(diào)度器中的P是processor的縮寫,它指的是一種可以承載若干個G、并且能夠使這些G適時地與M進行對接,并得到真正運行的中介。
這里的G正式goroutine的縮寫,而M則是machine的縮寫,M是系統(tǒng)級的線程。正式因為P的存在,G和M才能夠進行靈活、高效的配對,從而實現(xiàn)強大的并發(fā)編程模型。
P存在的一個很重要的原因是為了分散并發(fā)程序的執(zhí)行壓力,而讓臨時對象池中的本地池列表的長度與P的數(shù)量相同的主要原因也是分散壓力。這里的壓力包括存儲和性能兩個方面。
回到數(shù)據(jù)結(jié)構(gòu),在本地池列表中的每個本地池都包含了3個字段,或者說組件:
具體的數(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.
}
每個本地池都對應(yīng)著一個P。一個goroutine想要真正運行就必須先與某個P產(chǎn)生關(guān)聯(lián),所以一個正在運行的goroutine必然會關(guān)聯(lián)著某個P。在程序調(diào)用臨時對象池的Put方法或Get方法的時候,總會先試圖從臨時對象池的本地池列表中獲取對應(yīng)的本地池,依據(jù)的就是與當前goroutine關(guān)聯(lián)的那個P的ID。就是說,有一個goroutine,就會有一個與之一一對應(yīng)的本地池,一個臨時對象池的Put方法或Get方法會根據(jù)它所在的goroutine關(guān)聯(lián)到P,然后獲取到那個對應(yīng)的本地池。
Put方法,會先試圖把新的臨時對象存儲到本地池的private字段中。這樣在需要獲取臨時對象的時候,可以快速的拿到一個可用的值。只有當private字段已經(jīng)存有某個值的時候,Put方法才會去訪問本地池的shared字段進行存儲。
Get方法,會先試圖從本地池private字段出獲取一個臨時對象。只有當private字段的值為nil時,才會去訪問本地池的shared字段獲取對象。
shared字段,原則上可以被任何goroutine中的代碼訪問到,字段類型是切片,可以存放一組臨時對象。
private字段,只可能被與之對應(yīng)的P所關(guān)聯(lián)的goroutine中的代碼訪問到,可以說它是P級私有的。字段里只能存放一個臨時對象。
在訪問本地池的shared字段時,由于shared字段是共享的,必須受到互斥鎖的保護,這個鎖正是在結(jié)構(gòu)體中嵌入的Mutex。而訪問本地池的private字段是,不需要保護,所以這個private字段的存在是為了提供運行效率的。
在回到Put方法個Get方法,Get方法只需要去訪問與之對應(yīng)的本地池,先試著往private里存,如果已經(jīng)有了就存到shared里。而Get方法,在訪問過對應(yīng)的本地池的private和shared之后仍沒有獲取到任何對象,那么就會去訪問臨時對象池中的所有本地池,這些本地池都在本地池列表里。由于不是與之對應(yīng)了本地池了,只能訪問shared字段,嘗試獲取到一個對象。這一步也是可能無法獲取到一個可用的臨時對象的,可能是都被取走了,也可能是剛被大清洗過。還沒有的話,就是最后一個手段了,調(diào)用對象池(sync.Pool)里New字段的函數(shù)創(chuàng)建一個新的臨時對象。另外New字段是需要在初始化對象池的時候給定的,否則會返回nil,這樣的話Get方法也只能返回nil了。
本篇主要是一些概念,臨時對象池的使用起來還是比較簡單的。
下面的例子中,關(guān)于臨時對象池使用的代碼并不多,本身也并不需要多少。代碼中花了很大的精力在構(gòu)造一個讀寫緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu),真正需要使用到臨時對象的時候,往往有現(xiàn)成的對象可用,或是已經(jīng)在別處定義好對象了。不過作為一個demo還是很完整的,值得參考:
package main
import (
"io"
"bytes"
"fmt"
"sync"
)
// 存放數(shù)據(jù)塊緩沖區(qū)的臨時對象
var bufPool sync.Pool
// 預定義定界符
const delimiter = '\n'
// 一個簡易的數(shù)據(jù)庫緩沖區(qū)的接口
type Buffer interface {
Delimiter() byte // 獲取數(shù)據(jù)塊之間的定界符
Write(contents string) (err error) // 寫入一個數(shù)據(jù)塊
Read() (contents string, err error) // 讀取一個數(shù)據(jù)塊
Free() // 釋放當前的緩沖區(qū)
}
// 實現(xiàn)一個上面定義的接口
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}
},
}
}
// 獲取一個數(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)寫入,準備把數(shù)據(jù)讀出")
for {
block, err := buf.Read()
if err != nil {
if err == io.EOF {
break
}
panic(fmt.Errorf("讀取緩沖區(qū)時ERROR: %s", err))
}
fmt.Print(block)
}
}
sync.Pool類型是一個比較有用的同步工具,它的值被稱為臨時對象池。
臨時對象池有一個New字段,在初始化的時候最好給定它,是一個用來創(chuàng)建臨時對象的函數(shù)。臨時對象池還有兩個方法:Put和Get,分別用于向池中存放和獲取臨時對象。
還分析了臨時對象池內(nèi)部存儲臨時對象值的數(shù)據(jù)結(jié)構(gòu),正是因為有這樣的一個數(shù)據(jù)結(jié)構(gòu)的支撐,臨時對象池才能夠有效的分散存儲壓力和性能壓力。通過分析臨時對象池存取值的過程,了解到Get方法對這個數(shù)據(jù)結(jié)構(gòu)的妙用,使得其中的臨時對象可以被高效的利用。
這樣的內(nèi)部結(jié)構(gòu)和存取方式,讓臨時對象池成為了一個特點鮮明的同步工具。它存儲的臨時對象都應(yīng)該是擁有較長生命周期的值,并且這些值不應(yīng)該被某個goroutine中的代碼長期持有和使用。
創(chuàng)新互聯(lián)www.cdcxhl.cn,專業(yè)提供香港、美國云服務(wù)器,動態(tài)BGP最優(yōu)骨干路由自動選擇,持續(xù)穩(wěn)定高效的網(wǎng)絡(luò)助力業(yè)務(wù)部署。公司持有工信部辦法的idc、isp許可證, 機房獨有T級流量清洗系統(tǒng)配攻擊溯源,準確進行流量調(diào)度,確保服務(wù)器高可用性。佳節(jié)活動現(xiàn)已開啟,新人活動云服務(wù)器買多久送多久。