最近收到《程序员升级打怪兽》知识星球[1]的提问:《Go协程是轻量级线程,是否需要做多路复用增加工作量?性能能提高多少?》先说结论,Go的coroutinegoroutine非常轻量,这也是Go天然支持高并发的主要原因。但是协程goroutine频繁的创建和销毁给GC带来了很大的压力,会影响性能。grpool的作用是复用goroutines,减少频繁创建和销毁带来的性能损失。与goroutine相比,grpool更节省内存,但耗时更长;原因也很简单:grpool复用了协程,减少了协程的创建和销毁,减少了内存消耗;也因为协程的复用,总的协程数量减少,导致耗时变长。(一起工作的同事少了,项目就会延期,这个很好理解。)因此:GoFrame的grpool可以通过协程复用来节省内存。结合我们的需求:如果你的服务器内存不高或者业务场景需要更高的内存占用,那就用grpool。如果服务器内存足够但对耗时要求高,使用原生goroutine。名词解释Pool:goroutine池,用于管理几个可复用的goroutine协程资源Worker:pool中参与任务执行的goroutine对象,一个worker可以执行多个作业,直到队列中没有等待的作业为止Job:加入池中的任务在对象的任务队列中等待执行的是一个func()方法,一个job一次只能被一个worker获取并执行。使用示例使用默认的协程池,限制100个协程执行1000个任务github.com/gogf/gf/os/grpool""github.com/gogf/gf/os/gtimer""sync""time")funcmain(){pool:=grpool.New(100)//加1000我的任务:=0;我<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{}}//任务方法funcjob(){time.Sleep(time.Second)}打印结果是不是很简单?简单的走坑场景,请使用协程打印0~9。经常出错,请看下面代码是否有问题,请预测打印结果。wg:=sync.WaitGroup{}fori:=0;我<9;i++{wg.Add(1)gofunc(){fmt.Println(i)wg.Done()}()}wg.Wait()不要急于阅读答案...猜猜打印结果是什么.打印结果分析原因对于异步线程/协程,当一个函数注册异步执行时,该函数并没有真正开始执行。(在注册的时候,goroutine的栈中只保存了变量i的内存地址)一旦开始执行,函数就会读取变量i的值,此时变量i的值已增加到9。正确的写法wg:=sync.WaitGroup{}fori:=0;我<9;i++{wg.Add(1)gofunc(vint){fmt.Println(v)wg.Done()}(i)}wg.Wait()打印结果使用grpool使用grpool和使用go一样,你需要将当前变量i的值赋给一个不会改变的临时变量,并在函数中使用这个临时变量,而不是直接使用变量i。正确的代码wg:=sync.WaitGroup{}fori:=0;我<9;i++{wg.Add(1)v:=i//grpool.add()只能是无参匿名函数所以只能通过设置临时变量赋值_=grpool.Add(func(){fmt.Println(v)wg.Done()})}wg.Wait()打印结果错误码注意:这是一个错误的演示,不要这样写~wg:=sync.WaitGroup{}fori:=0;我<9;i++{wg.Add(1)_=grpool.Add(func(){fmt.Println(i)//打印结果都是9wg.Done()})}wg.Wait()打印结果性能测试使用for循环,开启10000个协程,使用原生goroutine和grpool分别执行。看两者在内存占用和耗时上的区别。packagemainimport("flag""fmt""github.com/gogf/gf/os/grpool""github.com/gogf/gf/os/gtime""log""os""runtime""runtime/pprof""sync""time")funcmain(){//接收命令行参数flag.Parse()//cpu分析cpuProfile()//主逻辑//demoGrpool()demoGoroutine()//内存分析memProfile()}funcdemoGrpool(){start:=gtime.TimestampMilli()wg:=sync.WaitGroup{}fori:=0;我<10000;i++{wg.Add(1)_=grpool.Add(func(){varmruntime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("内存占用:%dKb\n",m.Alloc/1024)time.Sleep(time.Millisecond)wg.Done()})fmt.Printf("Runningcoroutine:",grpool.Size())}wg.Wait()fmt.Printf("运行时间:%vms\n",gtime.TimestampMilli()-start)select{}}funcdemoGoroutine(){//start:=gtime.TimestampMilli()wg:=sync.WaitGroup{}fori:=0;我<10000;i++{wg.Add(1)gofunc(){//varmruntime.MemStats//runtime.ReadMemStats(&m)//fmt.Printf("运行时内存占用:%dKb\n",m.Alloc/1024)time.Sleep(time.Millisecond)wg.Done()}()}wg.Wait()//fmt.Printf("运行时间:%vms\n",gtime.TimestampMilli()-start)}varcpuprofile=flag.String("cpuprofile","","写cpu配置文件`file`")varmemprofile=flag.String("memprofile","","writememoryprofileto`file`")funccpuProfile(){if*cpuprofile!=""{f,err:=os.Create(*cpuprofile)iferr!=nil{log.Fatal("无法创建CPU配置文件:",err)}iferr:=pprof.StartCPUProfile(f);err!=nil{//monitorcpulog.Fatal("couldnotstartCPUprofile:",err)}deferpprof.StopCPUProfile()}}funcmemProfile(){if*memprofile!=""{f,err:=os.Create(*memprofile)iferr!=nil{log.Fatal("couldnotcreatememoryprofile:",err)}runtime.GC()//GC,获取最新的数据信息如果错误:=pprof.WriteHeapProfile(f);err!=nil{//写入内存信息log.Fatal("couldnotwritememoryprofile:",err)}f.Close()}}运行结果组件占用内存耗时grpool2229Kb1679msgoroutine5835Kb1258ms性能分析测试结果通过测试结果,我们可以清楚的看到,在相同的环境下执行相同的任务:grpool相比goroutine,占用内存更少,耗时更长;与grpool相比,占用内存多,耗时少。总结让我们回顾一下开篇文章的结论。相信通过仔细阅读,你一定会有更深的体会:Go的coroutinegoroutine是非常轻量级的,这也是Go的天然支持。高并发的主要原因。但是协程goroutine频繁的创建和销毁给GC带来了很大的压力,会影响性能。grpool的作用是复用goroutines,减少频繁创建和销毁带来的性能损失。与goroutine相比,grpool更节省内存,但耗时更长;原因也很简单:grpool复用了协程,减少了协程的创建和销毁,减少了内存消耗;也因为协程的复用,总的协程数量减少,导致耗时变长。(一起工作的同事少,项目就会延期,这个很好理解。)因此:goframe的grpool可以通过协程的复用来节省内存。结合我们的需求:如果你的服务器内存不高或者业务场景需要更高的内存占用,那就用grpool。如果服务器内存足够但对耗时要求高,使用原生goroutine。文章容易出错的代码部分可以再消化一下。参考资料[1]《程序员升级打怪兽》知识星球:https://wx.zsxq.com/dweb2/index/group/15528828844882欢迎来到StarGoFrame:https://github.com/gogf/gf本文为转载自微信公众号《程序员升级打怪之旅》,作者“王中阳围棋”,可通过以下二维码关注。转载本文请联系《程序员升级打怪之旅》公众号。
