GoFrame的grpool通過協程復用,能夠節省內存。結合我們的需求:如果你的服務器內存不高或者業務場景對內存占用的要求更高,那就使用grpool。如果服務器的內存足夠,但是對耗時有較高的要求,就用原生的goroutine。
最近收到「程序員升級打怪」知識星球[1]的提問:“go協程本來就是輕量級線程,還有必要做復用增加工作量嗎,性能可以提升多少呢?”
先說結論
- Go的協程goroutine非常輕量級,這也是Go天生支持高并發的主要原因。
- 但是協程goroutine頻繁的創建銷毀對GC的壓力比較大,會影響性能。
- grpool的作用就是復用goroutine,減少頻繁創建銷毀的性能損耗。grpool相比于goroutine更節省內存,但是耗時更長;
- 原因也很簡單:grpool復用了協程,減少了協程的創建和銷毀,減少了內存消耗;也因為協程的復用,總的協程數量減少,導致耗時變長。(一起干活的同事變少了,項目不就延期了嘛,很好理解。)
- 所以:GoFrame的grpool通過協程復用,能夠節省內存。結合我們的需求:如果你的服務器內存不高或者業務場景對內存占用的要求更高,那就使用grpool。如果服務器的內存足夠,但是對耗時有較高的要求,就用原生的goroutine。
名詞解釋
Pool: goroutine池,用于管理若干可復用的goroutine協程資源
Worker: 池對象中參與任務執行的goroutine,一個worker可以執行若干個job,直到隊列中再無等待的job
Job:添加到池對象的任務隊列中等待執行的任務,是一個func()方法,一個job同時只能被一個worker獲取并執行。
使用示例
使用默認的協程池,限制100個協程執行1000個任務
pool.Size() 獲得當前工作的協程數量
pool.Jobs() 獲得當前池中待處理的任務數量
package main
import (
"fmt"
"github.com/gogf/gf/os/grpool"
"github.com/gogf/gf/os/gtimer"
"sync"
"time"
)
func main() {
pool := grpool.New(100)
//添加1千個任務
for i := 0; i < 1000; i++ {
_ = pool.Add(job)
}
fmt.Println("worker:", pool.Size()) //當前工作的協程數量
fmt.Println("jobs:", pool.Jobs()) //當前池中待處理的任務數量
gtimer.SetInterval(time.Second, func() {
fmt.Println("worker:", pool.Size()) //當前工作的協程數
fmt.Println("jobs:", pool.Jobs()) //當前池中待處理的任務數
})
//阻止進程結束
select {}
}
//任務方法
func job() {
time.Sleep(time.Second)
}
打印結果

是不是灰常簡單~
踩坑之旅
一個簡單的場景,請使用協程打印0~9。
常犯的錯誤
大家看下面的代碼有沒有問題,請預測一下打印結果。
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
不用著急看答案
.
.
.
猜一下打印結果是什么。
打印結果

分析原因
對于異步線程/協程來講,函數進行異步執行注冊時,該函數并未真正開始執行。
(注冊時只在goroutine?的棧中保存了變量i的內存地址)
而一旦開始執行時函數才會去讀取變量i?的值,而這個時候變量i?的值已經自增到了9。
正確寫法
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
go func(v int) {
fmt.Println(v)
wg.Done()
}(i)
}
wg.Wait()
打印結果

使用grpool
使用grpool和使用go一樣,都需要把當前變量i的值賦值給一個不會改變的臨時變量,在函數中使用該臨時變量而不是直接使用變量i。
正確代碼
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
v := i //grpool.add() 的參數只能是不帶參數的匿名函數 因此只能以設置臨時變量的方式賦值
_ = grpool.Add(func() {
fmt.Println(v)
wg.Done()
})
}
wg.Wait()
打印結果

錯誤代碼
注意:這是錯誤的演示,不要這么寫~
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
_ = grpool.Add(func() {
fmt.Println(i) //打印結果都是9
wg.Done()
})
}
wg.Wait()
打印結果

性能測試
使用for循環,開啟一萬個協程,分別使用原生goroutine和grpool執行。
看兩者在內存占用和耗時方面的差別。
package main
import (
"flag"
"fmt"
"github.com/gogf/gf/os/grpool"
"github.com/gogf/gf/os/gtime"
"log"
"os"
"runtime"
"runtime/pprof"
"sync"
"time"
)
func main() {
//接收命令行參數
flag.Parse()
//cpu分析
cpuProfile()
//主邏輯
//demoGrpool()
demoGoroutine()
//內存分析
memProfile()
}
func demoGrpool() {
start := gtime.TimestampMilli()
wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
wg.Add(1)
_ = grpool.Add(func() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("運行中占用內存:%d Kb\n", m.Alloc/1024)
time.Sleep(time.Millisecond)
wg.Done()
})
fmt.Printf("運行的協程:", grpool.Size())
}
wg.Wait()
fmt.Printf("運行的時間:%v ms \n", gtime.TimestampMilli()-start)
select {}
}
func demoGoroutine() {
//start := gtime.TimestampMilli()
wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
//var m runtime.MemStats
//runtime.ReadMemStats(&m)
//fmt.Printf("運行中占用內存:%d Kb\n", m.Alloc/1024)
time.Sleep(time.Millisecond)
wg.Done()
}()
}
wg.Wait()
//fmt.Printf("運行的時間:%v ms \n", gtime.TimestampMilli()-start)
}
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
func cpuProfile() {
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil { //監控cpu
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
}
func memProfile() {
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
runtime.GC() // GC,獲取最新的數據信息
if err := pprof.WriteHeapProfile(f); err != nil { // 寫入內存信息
log.Fatal("could not write memory profile: ", err)
}
f.Close()
}
}
運行結果
組件
| 占用內存
| 耗時
|
grpool
| 2229 Kb
| 1679 ms
|
goroutine
| 5835 Kb
| 1258 ms
|
性能測試結果分析
通過測試結果我們能很明顯的看出來,在相同的環境下執行相同的任務:
grpool相比于goroutine,內存占用更少,耗時更長;
goroutine相比于grpool占用內存更高,耗時更短。
總結
我們再來回顧一下開篇的結論,相信通過仔細閱讀,你一定有了更好的理解:
- Go的協程goroutine非常輕量級,這也是Go天生支持高并發的主要原因。
- 但是協程goroutine頻繁的創建銷毀對GC的壓力比較大,會影響性能。
- grpool的作用就是復用goroutine,減少頻繁創建銷毀的性能損耗。grpool相比于goroutine更節省內存,但是耗時更長;
- 原因也很簡單:grpool復用了協程,減少了協程的創建和銷毀,減少了內存消耗;也因為協程的復用,總的協程數量減少,導致耗時變長。(一起干活的同事變少了,項目不就延期了嘛,很好理解。)
- 所以:goframe的grpool通過協程復用,能夠節省內存。結合我們的需求:如果你的服務器內存不高或者業務場景對內存占用的要求更高,那就使用grpool。如果服務器的內存足夠,但是對耗時有較高的要求,就用原生的goroutine。
- 文中的易錯代碼部分可以再重點消化一下。
參考資料
[1]「程序員升級打怪」知識星球: https://wx.zsxq.com/dweb2/index/group/15528828844882
歡迎Star GoFrame:https://github.com/gogf/gf
本文轉載自微信公眾號「 程序員升級打怪之旅」,作者「王中陽Go」,可以通過以下二維碼關注。

轉載本文請聯系「 程序員升級打怪之旅」公眾號。