gf框架之grpool - 高性能的goroutine池


Go語言中的goroutine雖然相對於系統線程來說比較輕量級,但是在高並發量下的goroutine頻繁創建和銷毀對於性能損耗以及GC來說壓力也不小。充分將goroutine復用,減少goroutine的創建/銷毀的性能損耗,這便是grpool對goroutine進行池化封裝的目的。例如,針對於100W個執行任務,使用goroutine的話需要不停創建並銷毀100W個goroutine,而使用grpool也許底層只需要幾千個goroutine便能充分復用地執行完成所有任務。經測試,在高並發下grpool的性能比原生的goroutine高出幾倍到數百倍!並且隨之也極大地降低了內存使用率。

性能測試報告:http://johng.cn/grpool-performance/

方法列表

func Add(f func()) func Jobs() int func SetExpire(expire int) func SetSize(size int) func Size() int type Pool func New(expire int, sizes ...int) *Pool func (p *Pool) Add(f func()) func (p *Pool) Close() func (p *Pool) Jobs() int func (p *Pool) SetExpire(expire int) func (p *Pool) SetSize(size int) func (p *Pool) Size() int 

通過grpool.New方法創建一個goroutine池,並給定池中goroutine的有效時間,單位為,第二個參數為非必需參數,用於限定池中的工作goroutine數量,默認為不限制。需要注意的是,任務可以不停地往池中添加,沒有限制,但是工作的goroutine是可以做限制的。我們可以通過Size()方法查詢當前的工作goroutine數量,使用Jobs()方法查詢當前池中待處理的任務數量。同時,池的大小和goroutine有效期可以通過SetSize和SetExpire方法在運行時進行動態改變。

同時,為便於使用,grpool包提供了默認的goroutine池,直接通過grpool.Add即可往默認的池中添加任務,任務參數必須是一個 func()類型的函數/方法。

使用示例

1、使用默認的goroutine池,限制10個工作goroutine執行1000個任務。

https://gitee.com/johng/gf/blob/master/geg/os/grpool/grpool1.go

package main import ( "time" "fmt" "gitee.com/johng/gf/g/os/gtime" "gitee.com/johng/gf/g/os/grpool" ) func job() { time.Sleep(1*time.Second) } func main() { grpool.SetSize(10) for i := 0; i < 1000; i++ { grpool.Add(job) } gtime.SetInterval(2*time.Second, func() bool { fmt.Println("size:", grpool.Size()) fmt.Println("jobs:", grpool.Jobs()) return true }) select {} } 

這段程序中的任務函數的功能是sleep 1秒鍾,這樣便能充分展示出goroutine數量限制功能。其中,我們使用了gtime.SetInterval定時器每隔2秒鍾打印出當前默認池中的工作goroutine數量以及待處理的任務數量。

2、我們再來看一個新手經常容易出錯的例子

https://gitee.com/johng/gf/blob/master/geg/os/grpool/grpool2.go

package main import ( "fmt" "sync" "gitee.com/johng/gf/g/os/grpool" ) func main() { wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) grpool.Add(func() { fmt.Println(i) wg.Done() }) } wg.Wait() } 

我們這段代碼的目的是要順序地打印出0-9,然而運行后卻輸出:

10
10
10
10
10
10
10
10
10
10

為什么呢?這里的執行結果無論是采用go關鍵字來執行還是grpool來執行都是如此。原因是,對於異步線程/協程來講,函數進行進行異步執行注冊時,該函數並未真正開始執行(注冊時只在goroutine的棧中保存了變量i的內存地址),而一旦開始執行時函數才會去讀取變量i的值,而這個時候變量i的值已經自增到了10。 清楚原因之后,改進方案也很簡單了,就是在注冊異步執行函數的時候,把當時變量i的值也一並傳遞獲取;或者把當前變量i的值賦值給一個不會改變的臨時變量,在函數中使用該臨時變量而不是直接使用變量i。

改進后的示例代碼如下:

1)、使用go關鍵字

https://gitee.com/johng/gf/blob/master/geg/os/grpool/grpool3.go

package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func(v int){ fmt.Println(v) wg.Done() }(i) } wg.Wait() } 

執行后,輸出結果為:

9
0
1
2
3
4
5
6
7
8

注意,異步執行時並不會保證按照函數注冊時的順序執行,以下同理。

2)、使用臨時變量

https://gitee.com/johng/gf/blob/master/geg/os/grpool/grpool4.go

package main import ( "fmt" "sync" "gitee.com/johng/gf/g/os/grpool" ) func main() { wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) v := i grpool.Add(func() { fmt.Println(v) wg.Done() }) } wg.Wait() } 

執行后,輸出結果為:

9
0
1
2
3
4
5
6
7
8

這里可以看到,使用grpool進行任務注冊時,只能使用func()類型的參數,因此無法在任務注冊時把變量i的值注冊進去,因此只能采用臨時變量的形式來傳遞當前變量i的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM