grpool协程池—控制goroutine数量利器

目的

grpool的目的就是控制执行任务的goroutine数量,避免创建过多的goroutine导致内存占用飙升

简单使用

Simple example

package main

import (
  "fmt"
  "runtime"
  "time"

  "github.com/ivpusic/grpool"
)

func main() {
  // number of workers, and size of job queue
  pool := grpool.NewPool(100, 50)

  // release resources used by pool
  defer pool.Release()

  // submit one or more jobs to pool
  for i := 0; i < 10; i++ {
    count := i

    pool.JobQueue <- func() {
      fmt.Printf("I am worker! Number %d\n", count)
    }
  }

  // dummy wait until jobs are finished
  time.Sleep(1 * time.Second)
}

Example with waiting jobs to finish

package main

import (
  "fmt"
  "runtime"

  "github.com/ivpusic/grpool"
)

func main() {
  // number of workers, and size of job queue
  pool := grpool.NewPool(100, 50)
  defer pool.Release()

  // how many jobs we should wait
  pool.WaitCount(10)

  // submit one or more jobs to pool
  for i := 0; i < 10; i++ {
    count := i

    pool.JobQueue <- func() {
      // say that job is done, so we can know how many jobs are finished
      defer pool.JobDone()

      fmt.Printf("hello %d\n", count)
    }
  }

  // wait until we call JobDone for all jobs
  pool.WaitAll()
}

源码分析

关键API

func NewPool(numWorkers int, jobQueueLen int) *Pool
func (p *Pool) Release()
func (p *Pool) WaitCount(count int)
func (p *Pool) JobDone()
func (p *Pool) WaitAll()

NewPool:创建worker goroutine池,numWorkers代表池中创建的worker数量,jobQueueLen代表可以接收的任务数量如果满了那么会阻塞

Pool.Release:释放池中创建的worker goroutine,注意这里没有释放jobQueue中的job

Pool.WaitCount:设置等待完成的任务数

Pool.JobDone:每次任务完成的时候应该调用这个方法,将等待完成的任务数减1

Pool.WaitAll:阻塞等待所有任务完成,当等待完成的任务数为0时继续往后执行

数据结构

在这里插入图片描述
Pool
goroutine池,包含三个属性JobQueue、Dispatcher、wg
JobQueue:任务队列,所有的任务按照先进先出的顺序执行,Dispatcher和Pool共享一个JobQueue
Dispatcher:任务分发器,负责从workerPool中取出空闲的worker、从jobQueue中取出job,将job交给worker执行
wg:用于等待任务完成

Job
要执行的任务,是一个无参无返回值的方法

dispatcher
任务分发器,包含三个属性workerPool、jobQueue、stop
workerPool:worker队列
jobQueue:任务队列
stop:无缓冲channel,用于传输stop信号

worker
执行任务的goroutine,包含三个属性workerPool、jobChannel、stop
workerPool:worker队列,后面讲解源码流程会提到为什么worker要引用worker队列
jobChannel:worker自身的无缓冲channel,用于传输worker要执行的任务
stop:无缓冲channel,用于传输stop信号

关键API源码分析

NewPool
func NewPool(numWorkers int, jobQueueLen int) *Pool {
    jobQueue := make(chan Job, jobQueueLen)
    workerPool := make(chan *worker, numWorkers)

    pool := &Pool{
        JobQueue:   jobQueue,
        dispatcher: newDispatcher(workerPool, jobQueue),
    }

    return pool
}

创建job队列、worker池,然后创建dispatcher任务分发器,最后创建Pool池对象

这里newDispatcher是关键
在这里插入图片描述

newDispatcher
func newDispatcher(workerPool chan *worker, jobQueue chan Job) *dispatcher {
    d := &dispatcher{
        workerPool: workerPool,
        jobQueue:   jobQueue,
        stop:       make(chan struct{}),
    }

    for i := 0; i < cap(d.workerPool); i++ {
        worker := newWorker(d.workerPool)
        worker.start()
    }

    go d.dispatch()
    return d
}

创建dispatcher对象,然后创建指定数量的worker并且启动worker,最后开启一个goroutine做任务分发
在这里插入图片描述

newWorker
func newWorker(pool chan *worker) *worker {
    return &worker{
        workerPool: pool,
        jobChannel: make(chan Job),
        stop:       make(chan struct{}),
    }
}

创建worker对象,workerPool引用之前创建的worker池对象,jobChannel、stop都创建一个属于自己的无缓冲chan

worker.start
func (w *worker) start() {
    go func() {
        var job Job
        for {
            // worker free, add it to pool
            w.workerPool <- w

            select {
            case job = <-w.jobChannel:
                job()
            case <-w.stop:
                w.stop <- struct{}{}
                return
            }
        }
    }()
}

死循环,将自身放到worker池中,如果自己的job队列中有job那么取出job执行,如果没有job但是stopChan中有stop信号,取出stop信号再往stopChan中发送一个stop信号后结束

这里有几个注意点:
1)如果jobChannel中有任务并且stopChan中也有stop信号,事实上是随机选择一条路执行
2)每次循环都在往workerPool中放入worker自身,那么什么时候取出来呢?
3)worker已经从stopChan中接收了stop信号,为什么还要往stopChan中发送一个stop信号呢?

其中2、3这两个注意点的答案都在接下来的dispatcher.dispatch方法,因此我把worker.start的流程图也放到下面的dispatcher.dispatch方法讲解中

dispatcher.dispatch
func (d *dispatcher) dispatch() {
    for {
        select {
        case job := <-d.jobQueue:
            worker := <-d.workerPool
            worker.jobChannel <- job
        case <-d.stop:
            for i := 0; i < cap(d.workerPool); i++ {
                worker := <-d.workerPool

                worker.stop <- struct{}{}
                <-worker.stop
            }

            d.stop <- struct{}{}
            return
        }
    }
}

死循环,如果job队列中有job,那么从workerPool中获取空闲的worker,如果获取不到则一直阻塞等待,如果获取到那么将job扔到worker自己的job队列;如果job队列中没有job、stopChan中有stop信号,那么给每个worker的stopChan中发送stop信号停止worker,并且在给每个worker发送完stop信号后要再次从worker的stopChan中接收到stop信号才继续停止下一个worker,所有worker停止完成后再往dispatcher自己的stopChan中发送stop信号后结束

这里有几个关键点:

1)如果jobQueue中有任务并且stopChan中也有stop信号,事实上是随机选择一条路执行
2)dispatcher分发任务的时候,是从workerPool中取出一个worker然后将任务分发给这个worker,这里就解答了上面的第二个关注点,也是dispatcher和worker配合处理job的方式
3)停止一个worker后还要再从worker的stopChan中接收stop信号后才继续停止下一个worker,这里和上面的第三个关注点呼应,目的是保证一个worker关闭后再去关闭下一个worker
4)所有worker停止后,为什么dispatcher还要往自身的stopChan中发送一个stop信号后才返回呢?这个问题的答案在下面的Pool.Release方法中会有解答
在这里插入图片描述

Pool.Release
func (p *Pool) Release() {
    p.dispatcher.stop <- struct{}{}
    <-p.dispatcher.stop
}

释放workerPool中所有的worker

往dispatcher的stopChan中发送完stop信号后,还要再次从dispatcher的stopChan中接收一个stop信号后才返回,这里和上面的第四个关键点呼应,目的是保证workerPool中所有的worker都释放后,Release方法才返回

Pool.WaitCount

func (p *Pool) WaitCount(count int) {
	p.wg.Add(count)
}

设置等待完成的任务数

整个任务的等待执行就是通过sync.WaitGroup来实现的

Pool.JobDone

func (p *Pool) JobDone() {
	p.wg.Done()
}

每次任务完成的时候应该调用这个方法,将等待完成的任务数减1

Pool.WaitAll

func (p *Pool) WaitAll() {
	p.wg.Wait()
}

阻塞等待所有任务完成,当等待完成的任务数为0时继续往后执行

总结

整个grpool的设计比较简单,关键是搞懂worker.start、dispatcher.dispatch这两个方法是如何配合进行job的分发以及job的执行

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值