在golang的标准网络库当中,应对网络请求的方式是遇到请求直接起一个goroutine来处理,并且底层使用了基于epoll的io多路复用。
那么一个自然的问题,在已经有比较完备的GMP调度的前提下是否有必要去池化goroutine?答案是当所启动的goroutine较多时,池化复用的思想还是很有必要的,关于是否需要池化大家感兴趣的话可以搜索相关的资料研究一下,在这里我就goroutine池化做一下简单实用的实现:
- 首先我们得确定池子的大小
- 我们得有一个类似于队列的结构来存放我们需要处理的任务
我们的goroutine就像是一群嗷嗷待哺的小鸟,等待任务的投喂,也就是一群goroutine等待任务队列的任务,在确定了这两个基本点之后:任务的添加,任务的处理。也就有了后续的第一版goroutine pool:
这里我们使用通道来实现,Run方法就是开启固定尺寸的goroutine池子,然后每个goroutine都在抢任务,测试一下,:
截图如下所示:
可以看到在运行完毕,执行关闭操作之后,goroutine数量减三,符合我们的期望。关于关闭池子的方法我们在第二版之后介绍,第二个版本主要是是为了解决带参数的任务函数怎么池化的问题。
有了这个的问题的前提下,我们在想是否可以实现一个带参数的任务函数,也就有了我们的第二版:
在这里我们通过构建一个JobItem来包含处理函数和函数参数,然后添加任务的时候需要额外传入参数。
对于这样的池子,我们还缺少修改池子大小的能力,在修改池子大小的时候,我们可以以增量的方式灌水,什么意思呢?就是假设现在有5个goroutine的池子,我需要修改为10个,那么我只需要重新再启动5个就可以了,这样修改尺寸的功能也完成了。
关闭池子也很简单,我们需要关闭任务通道,那么我们这里使用的for range就不太合适,因为close一个channel之后,如果继续让里面send的话是会panic的,所以我们需要一个状态来处理closed channel
这样我们可以对这种情况做一些处理,不至于让程序直接panic死掉,我们清除掉系统正在运行的goroutine,手动调用runtime.Goexit( ).
ps: 关于实现方法我在这里提几点可优化思路:
- 使用sync.WaitGroup去替代上面的手动清理goroutine,转而去使用wg.Done,更加的优雅。
- 可以使用context包去让我们启动的goroutine都继承一个rootCtx,然后利用返回的cancel函数去结束掉我们的goroutine,当然这里我们也是要使用ctx.Done()方法来调用wg.Done.具体实现细节可以多种方式互用。
- todo,这里说一下我目前想到的需要完善的点,关于AddTask的灵活性的问题,假如我们想对任意函数进行添加然后处理,实现一个无差别处理池,而不是针对特定任务的池子,该怎么去做?