协程池

277 阅读5分钟

协程池

每次任务的执行,都去go一个新的协程,这其中会可能会牵扯到,协程的创建分配调度等一些列流程,可能对我们的 内存和cpu利用率都不友好,为了减少资源的浪费,我们可以将 协程 进行 池** 化,复用协程, 减少资源过度浪费

一个协程应该具备那些基本的功能

  • 执行协程的 panic处理
  • 自定义协程池的大小
  • 协程任务的等待执行

参考链接

go-playground/pool.v3

一个功能简单易用的协程池,var poolWorks = pool.NewLimited(num) , 当创建了 限制limt了后,每次业务处理最多只有 num个work的goroutine在 并发处理。

功能

  • 支持unlimt 和 limit的创建协程池
  • 支持协程的cancel
  • 协程recover的处理
  • 隐藏处理细节,使用简单,固定套路和流程 Batch->Queue->QueueComplete->Results
  • 协程的创建是 "饿汉模式", 程序在初始化的就创建好的协程了

执行流程

go-playground_pool.v3

0、在初始化协程池时候会异步创建 newWorker 来处理 任务

0、创建batch对象,将task到 queue时,都是异步添加

0、等待并阻塞这直到所有的任务完成

Demo

var worker = pool.NewLimited(2)
​
func main() {
  // defer worker.close()
  batch := worker.Batch()
  
  batch.Queue(func(wu pool.WorkUnit) (interface{}, error) { // 往队列里面写入是异步操作
    getUserInfo()
    return "Queue-getUserInfo", nil
  })
  batch.Queue(func(wu pool.WorkUnit) (interface{}, error) {
    getTeacher()
    return "Queue-getTeacher", nil
  })
  
  // 告诉batch 没有要执行的 func, 当work执行完毕 close(result)避免一直阻塞
  batch.QueueComplete() 
  // 等待所有的 Queue 函数执行完毕,并获取返回值
  for ch := range batch.Results() {
    fmt.Println(ch.Value())
  }
}
​
func getUserInfo() {
  time.Sleep(1 * time.Second)
  fmt.Println("getUserInfo")
}
​
func getTeacher() {
  time.Sleep(5 * time.Second)
  fmt.Println("getTeacher")
}

ants

功能

  • 自动调度海量的 goroutines,复用 goroutines
  • 定期清理过期的 goroutines,进一步节省资源
  • 提供了大量有用的接口:任务提交、获取运行中的 goroutine 数量、动态调整 Pool 大小、释放 Pool、重启 Pool
  • 优雅处理 panic,防止程序崩溃
  • 资源复用,极大节省内存使用量;在大规模批量并发任务场景下比原生 goroutine 并发具有更高的性能
  • 非阻塞机制
  • 协程的创建是 “懒汉模式”,需要的时候就创建,

执行流程

ants-协程池

0、从流程图我们能发现,在初始化 工作池时,会启动一个goroutine来定时清理 过期worker,从而节省资源

0、每次worker执行完任务后,都会放到 workerArray 中,循环队列尾部/栈底,有新的任务则会从 循环队列头部/栈顶获取可执行worker

0、当执行任务 发生panic 我们也会 将 gowork对象 放到 对象池方便一些复用对象。

DEMO

  • 使用默认的 defaultAntsPool

适合执行不同的 任务task

func Test_dft(t *testing.T) {
  defer ants.Release()
  runTimes := 1000
  var wg sync.WaitGroup
  syncCalculateSum := func() {
    fmt.Print("hello world")
    wg.Done()
  }
  for i := 0; i < runTimes; i++ {
    wg.Add(1)
    ants.Submit(syncCalculateSum)
  }
  wg.Wait()
  fmt.Printf("running goroutines: %d\n", ants.Running()) // 可能会减1
  fmt.Printf("finish all tasks.\n")
}
  • 使用 池函数

适合一批goroutine执行 同一个任务函数,

var sum int32// ants的方式,
func Test_func(t *testing.T) {
  var wg sync.WaitGroup
  pf, _ := ants.NewPoolWithFunc(10, func(i interface{}) { 
    val, _ := i.(int32)
    atomic.AddInt32(&sum, val)
    wg.Done()
  })
  defer pf.Release()
  runTimes := 1000for i := 0; i < runTimes; i++ {
    wg.Add(1)
    pf.Invoke(int32(i))
  }
  wg.Wait()
  fmt.Printf("running goroutines: %d\n", pf.Running())
  fmt.Printf("finish all tasks, result is %d\n", sum)
}
  • opetions参数
type Options struct {
    // worker的过期时间,默认1s
    ExpiryDuration time.Duration
    // 是否要预分配对象,这个分配的对象是 workerArray【statk,queue】
    PreAlloc bool
    // 最大要阻塞的任务数量,默认不限制0
    MaxBlockingTasks int
    // 不阻塞等待任务执行
    Nonblocking bool
    // 自定义panicHandker处理函数
    PanicHandler func(interface{})
    // 自定义logger的处理
    Logger Logger
}

VS

  • 从功能上
功能Pool.v3ants
panic自定义处理函数
协程错误recover处理
定期清理过期woker
动态修改协程池子大小
对象创建复用
  • 从设计上

ants 使用 对象池sync.pool 和 定时器 来减少对象分配和 资源的浪费,可以指定 等待完成的任务数。

go-playground/pool.v3 就是简单生产者消费者,没有考虑 复用,每个task的任务的执行,强制性的等待完成才行。

  • 代码测试

网上一大堆就不举例了

总结

  • 优先选择 ant,因为ant的功能丰富,设计理念做到了 资源利用的最大化。自定义等待完成的任务数。worker数量动态扩增与缩减
  • ant任务的执行一定是 submit-> 获取可执行的goworker ->执行task -》 放到workerarray等待下一次的任务执行
  • ant中用的了sync.cond 来实现 协程的广播通知 和 通知某个正在阻塞等待协程

Q&A

ant-goroutine的goWorker-life

  • goroutine-task发生panic
  • 从goroutine-task中取出 nil(过期了)
  • workerarray没有空闲的free-wroker

ant-pool的容量可以为-1的

  • 在初始化pool是如何设置为 size 小于等0则 设置为-1
  • 如果你设置了预分配workerArray,PreAlloc=true,则设置-1则提现错误

ant-为什么高效

  • 在初始化时候 启动一个goroutine,会启动一个goroutine来定时清理 过期worker,从而节省资源
  • 每次创建的goWorker对象是从 对象池创建,保证了对象的复用

ants-用栈和用循环队列区别

  • 使用loopQueueType循环队列则对容量又限制
  • 使用stackType栈则对容量没限制

ants-Running值的统计

  • 当有新的goWorker 被被run的的使用 +1
  • goWorker 执行task,发生painc 或者 从task中读取为nil值(过期了)或者 没有剩余的workerarray空间时候会 running -1

获取过期的worker

nowTime=10点

expireTime = 1h

workerarray【worker9-7点,worker8-8点,worker7-9点,worker6-10点】

因为该 workeryarray是有序的,所以是用二分查找发找到过期的 worker的分界线

  • stackType

0、设置过期的 expire worerks

0、删除过期的expire workers从 items中, iterms[i] = nil

0、重新调整 items

ant-从workerCache中获取对象不需要reset

type goWorker struct {
  // pool who owns this worker.
  pool *Pool
  // task is a job should be done.
  task chan func()
  // recycleTime will be updated when putting a worker back into queue.
  recycleTime time.Time
}

0、每个goWorker都共享这个一个 pool,所以不能reset

0、goWorker.task 是一个 make(chan func,1),当从task中取出数据后,task里面就是无数据的了,所以不需要reset

0、每个submit的任务的提交,都会尝试从 wokerarray中获取下一个可执行的 worker,

0、执行完task 后 每次都会 revertWorker 放进 wokerarray 中 来重新设置该值,

业务中如何合理设置池大小

方案一【不限制池子大小】

如果对机器的性能没有什么要求则,则可以直接采用 不限制池的大小的方法来使用协程池,因为ants本身也提供了,【定期删除 不适用worker】,所以也不需要担心的资源的浪费

方案二【限制池子大小】

对资源使用的更加精细化的分配使用,可以使用一下思路来设置我们的池子大小

  • 根据监控统计该接口qps,根据qps可是设置一个 初始值
  • 在代码中设置动态库容机制
// 方法1,启动一个定时器,定期监测判断是否需要扩容,
// 方法2,包装 ants,新增一个 自定义动态扩容函数,然后启动定时器,不间断的执行用户自己定义的扩容函数
func DynamicResize() { 
  // 等待的数量已经超过我本身容量的 1/4了则触发扩容机制
  // 可能我的自己的设置的 size不合适
  if antsPool3.Waiting() > antsPool3.Cap()/4 {
    antsPool3.Tune(antsPool3.Waiting()/4 + antsPool3.Cap())
  }
}
  • 将 Running 的数量的也进行上报,后期也可以参考 running的均值,来重新设置 池子的 cap