在现代高并发的Web服务中,合理有效地使用和管理Goroutine资源对于提升系统性能至关重要。GoFrame作为一个模块化、高性能的Go语言Web框架,提供了grpool工具来方便地管理Goroutine池。本文将介绍grpool的基本原理和使用方法。
grpool概述
grpool是GoFrame框架提供的Goroutine池管理模块。其作用是预先创建一定数量的Goroutine,当有任务需要异步处理时,直接从池中取出一个Goroutine来执行,而不是临时创建新的Goroutine。任务执行完毕后,Goroutine将被放回池中,而不是销毁,从而避免频繁创建和销毁Goroutine的开销。这在高并发场景下可以显著提升系统性能。
使用grpool
1. 创建goroutine池对象
首先,需要创建一个goroutine池的对象。可以使用默认的Pool对象,也可以使用New方法自定义池的参数:
import "github.com/gogf/gf/v2/os/grpool"
// 使用默认池对象
grpool.Add(ctx, func(ctx context.Context) {
// handle something
})
// 自定义池参数
pool := grpool.New(100)
pool.Add(ctx, func(ctx context.Context) {
// handle something
})
New方法的参数含义如下:
- 第1个参数:Goroutine池的初始大小,默认不限制
2. 提交任务
可以使用Add方法向池中提交要异步执行的任务。任务需要定义为一个函数:
pool.Add(ctx, func(ctx context.Context) {
// 异步执行的任务逻辑
time.Sleep(time.Second)
fmt.Println("hello")
})
Add方法是非阻塞的,它会立即返回而不等待任务执行完成。
3.关闭与等待
任务提交完成后,可以调用Close方法关闭池。之后池将不再接收新的任务,但已提交的任务会被执行完成。
pool.Close()
需要注意,一旦关闭池,就不能再提交新任务了,否则会触发panic。
4. 使用示例
下面是一个简单的示例程序,演示了如何使用grpool来并发下载一组图片:
func main() {
ctx := gctx.New()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
grpool.Add(ctx, func(ctx context.Context) {
defer wg.Done()
downloadImage(fmt.Sprintf("https://example.com/image/%d", i))
})
}
wg.Wait()
}
func downloadImage(url string) {
// 下载图片
fmt.Println("download done", url)
}
这个例子启动了10个并发的任务去下载图片。downloadImage函数实际执行下载逻辑(这里为了简化只打印了一条日志)。主Goroutine通过wg.Wait等待所有任务执行完成。
性能分析
那么使用grpool相比直接go func()启动Goroutine有什么优势呢?我们可以做一个性能对比测试:
// 直接启动Goroutine
func BenchmarkWithoutPool(b *testing.B) {
ctx := gctx.New()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
task()
}()
}
wg.Wait()
}
// 使用goroutine池
func BenchmarkWithPool(b *testing.B) {
ctx := gctx.New()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
grpool.Add(ctx, func(ctx context.Context) {
defer wg.Done()
task()
})
}
wg.Wait()
}
func task() {
time.Sleep(time.Millisecond * 100)
}
在我的机器上运行基准测试的结果如下:
BenchmarkWithoutPool-8 3662 304738 ns/op
BenchmarkWithPool-8 5804 202395 ns/op
可以看到,使用grpool的每次操作耗时约为不使用池的66%,性能有明显提升。这是因为创建和销毁Goroutine也是需要一定开销的,频繁的创建销毁会影响性能。而使用grpool复用Goroutine,规避了这部分开销。
数据结构
grpool中主要的数据结构:Pool。
type Pool struct {
limit int // Max goroutine count limit.
count *gtype.Int // Current running goroutine count.
list *glist.List // Job list for asynchronous job adding purpose.
closed *gtype.Bool // Is pool closed or not.
}
Pool结构体表示一个Goroutine池对象,它维护了当前池的状态信息,如当前goroutine的数量、等待执行的任务等。
创建Pool
创建一个Pool对象时,可以指定池的初始大小。如果使用默认的Pool对象,则这些参数取默认值。
func New(limit ...int) *Pool {
var (
pool = &Pool{
limit: -1,
count: gtype.NewInt(),
list: glist.New(true),
closed: gtype.NewBool(),
}
timerDuration = grand.D(
minSupervisorTimerDuration,
maxSupervisorTimerDuration,
)
)
if len(limit) > 0 && limit[0] > 0 {
pool.limit = limit[0]
}
gtimer.Add(context.Background(), timerDuration, pool.supervisor)
return pool
}
创建Pool时会启动定时器,每过一段时间就去取任务,使任务进入事件循环,不断等待接收任务。
提交任务
提交任务即调用Pool的Add方法。Add会先判断是否超出上限如果没有超出上限,goroutine。然后将任务函数发送到该goroutine去执行。
func (p *Pool) Add(ctx context.Context, f Func) error {
for p.closed.Val() {
return gerror.NewCode(
gcode.CodeInvalidOperation,
"goroutine defaultPool is already closed",
)
}
p.list.PushFront(&localPoolItem{
Ctx: ctx,
Func: f,
})
// Check and fork new worker.
p.checkAndForkNewGoroutineWorker()
return nil
}
func (p *Pool) checkAndForkNewGoroutineWorker() {
// Check whether fork new goroutine or not.
var n int
for {
n = p.count.Val()
if p.limit != -1 && n >= p.limit {
// No need fork new goroutine.
return
}
if p.count.Cas(n, n+1) {
// Use CAS to guarantee atomicity.
break
}
}
// Create job function in goroutine.
go p.asynchronousWorker()
}
func (p *Pool) asynchronousWorker() {
defer p.count.Add(-1)
var (
listItem interface{}
poolItem *localPoolItem
)
// Harding working, one by one, job never empty, worker never die.
for !p.closed.Val() {
listItem = p.list.PopBack()
if listItem == nil {
return
}
poolItem = listItem.(*localPoolItem)
poolItem.Func(poolItem.Ctx)
}
}
添加任务后池的goroutine数量jobs会原子性增加1。asynchronousWorker尝试从池的空闲列表中弹出一个worker,如果列表为空则返回nil。
设计亮点
grpool巧妙地运用Go语言的Goroutine、Atomic等特性,实现了高效的Goroutine池化管理,避免了Goroutine的频繁创建和销毁,能够显著提升高并发场景下的系统性能。同时grpool也提供了简洁易用的API,让用户可以方便地使用池进行任务的异步并发处理。
注意事项
使用grpool要注意以下几点:
- 池中Goroutine的数量是有限的,同一时刻只能最多处理Pool大小个任务,超出的任务要等池中有空闲Goroutine时才能被执行。
- Goroutine复用可以减少创建销毁的开销,但也不是越大越好。Goroutine过多会加重调度负担,也会占用更多内存。
- 不建议池中的Goroutine执行长时间运行的任务,以免长期占用,影响其他任务执行。
- 必要时可以调用Pool的Tune方法动态调整池的容量。当任务增多时扩容,任务减少时缩容,灵活应对不同负载。
总结
grpool为GoFrame提供了高效便捷的Goroutine池管理能力。合理使用池化技术能够显著提升系统性能,特别是在高并发的场景下。但同时也要避免滥用,根据实际情况选择Pool大小,权衡Goroutine数量与调度开销。总之,grpool是GoFrame提供的又一利器,能让gopher轻松驾驭Goroutine,更好地发挥Go语言高并发的优势。