新聞中心
最近收到「程序員升級(jí)打怪」知識(shí)星球[1]的提問(wèn):“go協(xié)程本來(lái)就是輕量級(jí)線程,還有必要做復(fù)用增加工作量嗎,性能可以提升多少呢?”

創(chuàng)新互聯(lián)專注骨干網(wǎng)絡(luò)服務(wù)器租用10多年,服務(wù)更有保障!服務(wù)器租用,綿陽(yáng)服務(wù)器托管 成都服務(wù)器租用,成都服務(wù)器托管,骨干網(wǎng)絡(luò)帶寬,享受低延遲,高速訪問(wèn)。靈活、實(shí)現(xiàn)低成本的共享或公網(wǎng)數(shù)據(jù)中心高速帶寬的專屬高性能服務(wù)器。
先說(shuō)結(jié)論
- Go的協(xié)程goroutine非常輕量級(jí),這也是Go天生支持高并發(fā)的主要原因。
- 但是協(xié)程goroutine頻繁的創(chuàng)建銷(xiāo)毀對(duì)GC的壓力比較大,會(huì)影響性能。
- grpool的作用就是復(fù)用goroutine,減少頻繁創(chuàng)建銷(xiāo)毀的性能損耗。grpool相比于goroutine更節(jié)省內(nèi)存,但是耗時(shí)更長(zhǎng);
- 原因也很簡(jiǎn)單:grpool復(fù)用了協(xié)程,減少了協(xié)程的創(chuàng)建和銷(xiāo)毀,減少了內(nèi)存消耗;也因?yàn)閰f(xié)程的復(fù)用,總的協(xié)程數(shù)量減少,導(dǎo)致耗時(shí)變長(zhǎng)。(一起干活的同事變少了,項(xiàng)目不就延期了嘛,很好理解。)
- 所以:GoFrame的grpool通過(guò)協(xié)程復(fù)用,能夠節(jié)省內(nèi)存。結(jié)合我們的需求:如果你的服務(wù)器內(nèi)存不高或者業(yè)務(wù)場(chǎng)景對(duì)內(nèi)存占用的要求更高,那就使用grpool。如果服務(wù)器的內(nèi)存足夠,但是對(duì)耗時(shí)有較高的要求,就用原生的goroutine。
名詞解釋
Pool: goroutine池,用于管理若干可復(fù)用的goroutine協(xié)程資源
Worker: 池對(duì)象中參與任務(wù)執(zhí)行的goroutine,一個(gè)worker可以執(zhí)行若干個(gè)job,直到隊(duì)列中再無(wú)等待的job
Job:添加到池對(duì)象的任務(wù)隊(duì)列中等待執(zhí)行的任務(wù),是一個(gè)func()方法,一個(gè)job同時(shí)只能被一個(gè)worker獲取并執(zhí)行。
使用示例
使用默認(rèn)的協(xié)程池,限制100個(gè)協(xié)程執(zhí)行1000個(gè)任務(wù)
pool.Size() 獲得當(dāng)前工作的協(xié)程數(shù)量
pool.Jobs() 獲得當(dāng)前池中待處理的任務(wù)數(shù)量
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千個(gè)任務(wù)
for i := 0; i < 1000; i++ {
_ = pool.Add(job)
}
fmt.Println("worker:", pool.Size()) //當(dāng)前工作的協(xié)程數(shù)量
fmt.Println("jobs:", pool.Jobs()) //當(dāng)前池中待處理的任務(wù)數(shù)量
gtimer.SetInterval(time.Second, func() {
fmt.Println("worker:", pool.Size()) //當(dāng)前工作的協(xié)程數(shù)
fmt.Println("jobs:", pool.Jobs()) //當(dāng)前池中待處理的任務(wù)數(shù)
})
//阻止進(jìn)程結(jié)束
select {}
}
//任務(wù)方法
func job() {
time.Sleep(time.Second)
}
打印結(jié)果
是不是灰常簡(jiǎn)單~
踩坑之旅
一個(gè)簡(jiǎn)單的場(chǎng)景,請(qǐng)使用協(xié)程打印0~9。
常犯的錯(cuò)誤
大家看下面的代碼有沒(méi)有問(wèn)題,請(qǐng)預(yù)測(cè)一下打印結(jié)果。
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()不用著急看答案
.
.
.
猜一下打印結(jié)果是什么。
打印結(jié)果
分析原因
對(duì)于異步線程/協(xié)程來(lái)講,函數(shù)進(jìn)行異步執(zhí)行注冊(cè)時(shí),該函數(shù)并未真正開(kāi)始執(zhí)行。
(注冊(cè)時(shí)只在goroutine?的棧中保存了變量i的內(nèi)存地址)
而一旦開(kāi)始執(zhí)行時(shí)函數(shù)才會(huì)去讀取變量i?的值,而這個(gè)時(shí)候變量i?的值已經(jīng)自增到了9。
正確寫(xiě)法
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
go func(v int) {
fmt.Println(v)
wg.Done()
}(i)
}
wg.Wait()打印結(jié)果
使用grpool
使用grpool和使用go一樣,都需要把當(dāng)前變量i的值賦值給一個(gè)不會(huì)改變的臨時(shí)變量,在函數(shù)中使用該臨時(shí)變量而不是直接使用變量i。
正確代碼
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
v := i //grpool.add() 的參數(shù)只能是不帶參數(shù)的匿名函數(shù) 因此只能以設(shè)置臨時(shí)變量的方式賦值
_ = grpool.Add(func() {
fmt.Println(v)
wg.Done()
})
}
wg.Wait()打印結(jié)果
錯(cuò)誤代碼
注意:這是錯(cuò)誤的演示,不要這么寫(xiě)~
wg := sync.WaitGroup{}
for i := 0; i < 9; i++ {
wg.Add(1)
_ = grpool.Add(func() {
fmt.Println(i) //打印結(jié)果都是9
wg.Done()
})
}
wg.Wait()打印結(jié)果
性能測(cè)試
使用for循環(huán),開(kāi)啟一萬(wàn)個(gè)協(xié)程,分別使用原生goroutine和grpool執(zhí)行。
看兩者在內(nèi)存占用和耗時(shí)方面的差別。
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() {
//接收命令行參數(shù)
flag.Parse()
//cpu分析
cpuProfile()
//主邏輯
//demoGrpool()
demoGoroutine()
//內(nèi)存分析
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("運(yùn)行中占用內(nèi)存:%d Kb\n", m.Alloc/1024)
time.Sleep(time.Millisecond)
wg.Done()
})
fmt.Printf("運(yùn)行的協(xié)程:", grpool.Size())
}
wg.Wait()
fmt.Printf("運(yùn)行的時(shí)間:%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("運(yùn)行中占用內(nèi)存:%d Kb\n", m.Alloc/1024)
time.Sleep(time.Millisecond)
wg.Done()
}()
}
wg.Wait()
//fmt.Printf("運(yùn)行的時(shí)間:%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 { //監(jiān)控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,獲取最新的數(shù)據(jù)信息
if err := pprof.WriteHeapProfile(f); err != nil { // 寫(xiě)入內(nèi)存信息
log.Fatal("could not write memory profile: ", err)
}
f.Close()
}
}
運(yùn)行結(jié)果
|
組件 |
占用內(nèi)存 |
耗時(shí) |
|
grpool |
2229 Kb |
1679 ms |
|
goroutine |
5835 Kb |
1258 ms |
性能測(cè)試結(jié)果分析
通過(guò)測(cè)試結(jié)果我們能很明顯的看出來(lái),在相同的環(huán)境下執(zhí)行相同的任務(wù):
grpool相比于goroutine,內(nèi)存占用更少,耗時(shí)更長(zhǎng);
goroutine相比于grpool占用內(nèi)存更高,耗時(shí)更短。
總結(jié)
我們?cè)賮?lái)回顧一下開(kāi)篇的結(jié)論,相信通過(guò)仔細(xì)閱讀,你一定有了更好的理解:
- Go的協(xié)程goroutine非常輕量級(jí),這也是Go天生支持高并發(fā)的主要原因。
- 但是協(xié)程goroutine頻繁的創(chuàng)建銷(xiāo)毀對(duì)GC的壓力比較大,會(huì)影響性能。
- grpool的作用就是復(fù)用goroutine,減少頻繁創(chuàng)建銷(xiāo)毀的性能損耗。grpool相比于goroutine更節(jié)省內(nèi)存,但是耗時(shí)更長(zhǎng);
- 原因也很簡(jiǎn)單:grpool復(fù)用了協(xié)程,減少了協(xié)程的創(chuàng)建和銷(xiāo)毀,減少了內(nèi)存消耗;也因?yàn)閰f(xié)程的復(fù)用,總的協(xié)程數(shù)量減少,導(dǎo)致耗時(shí)變長(zhǎng)。(一起干活的同事變少了,項(xiàng)目不就延期了嘛,很好理解。)
- 所以:goframe的grpool通過(guò)協(xié)程復(fù)用,能夠節(jié)省內(nèi)存。結(jié)合我們的需求:如果你的服務(wù)器內(nèi)存不高或者業(yè)務(wù)場(chǎng)景對(duì)內(nèi)存占用的要求更高,那就使用grpool。如果服務(wù)器的內(nèi)存足夠,但是對(duì)耗時(shí)有較高的要求,就用原生的goroutine。
- 文中的易錯(cuò)代碼部分可以再重點(diǎn)消化一下。
參考資料
[1]「程序員升級(jí)打怪」知識(shí)星球: https://wx.zsxq.com/dweb2/index/group/15528828844882
歡迎Star GoFrame:https://github.com/gogf/gf
本文轉(zhuǎn)載自微信公眾號(hào)「 程序員升級(jí)打怪之旅」,作者「王中陽(yáng)Go」,可以通過(guò)以下二維碼關(guān)注。
轉(zhuǎn)載本文請(qǐng)聯(lián)系「 程序員升級(jí)打怪之旅」公眾號(hào)。
分享文章:為什么Go搞了協(xié)程GoFrame還要搞協(xié)程池?怎么用?什么時(shí)候用?
當(dāng)前路徑:http://m.fisionsoft.com.cn/article/cogshdj.html


咨詢
建站咨詢
