GoFrame框架中的grpool - 高效管理Goroutine池

190 阅读5分钟

在现代高并发的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要注意以下几点:

  1. 池中Goroutine的数量是有限的,同一时刻只能最多处理Pool大小个任务,超出的任务要等池中有空闲Goroutine时才能被执行。
  2. Goroutine复用可以减少创建销毁的开销,但也不是越大越好。Goroutine过多会加重调度负担,也会占用更多内存。
  3. 不建议池中的Goroutine执行长时间运行的任务,以免长期占用,影响其他任务执行。
  4. 必要时可以调用Pool的Tune方法动态调整池的容量。当任务增多时扩容,任务减少时缩容,灵活应对不同负载。

总结

grpool为GoFrame提供了高效便捷的Goroutine池管理能力。合理使用池化技术能够显著提升系统性能,特别是在高并发的场景下。但同时也要避免滥用,根据实际情况选择Pool大小,权衡Goroutine数量与调度开销。总之,grpool是GoFrame提供的又一利器,能让gopher轻松驾驭Goroutine,更好地发挥Go语言高并发的优势。