掘金 后端 ( ) • 2024-04-30 11:35

theme: fancy

写在前面

字节跳动开源项目gopool是一个十分轻量且高性能的协程池,如果在业务上没有什么特殊需求,它可以满足大部分日常开发中对于协程池的使用需求。本次我们就来一次研究一下gopool的实现原理。

总体架构

未命名文件.png gopool的构成非常简单,由pool/worker/task三个对象协作完成协程池的工作。

  • pool:pool是协程池的一个完整实现,它通过 Pool interface 对用户暴露协程池操作方法。并且,管理着task和worker两个对象集合。
  • task:task是对协程池中的每个任务函数的封装,它通过自己的成员变量 f 来注册用户的待执行函数。同时,通过成员变量 next 来指向下一个待执行 task。pool和worker就是通过 task 构建的链表,来完成 task 的注册和task的取出执行。

image.png

  • worker:worker 是 task 的执行者,它在被启动(pool 去调用 worker 对象的 run 方法)后,会持续的从 task 链表中取出 task 对象,并执行其中的函数。一个 worker 一次只能执行一个 task,因此,worker的数量就可以代表当前 pool 中运行的任务总数。

image.png

使用gopool

gopool 的使用非常简单,只需要按如下方式将任务函数传入 pool 中,pool会自动创建 task 并管理 worker 去启动任务。因为 worker 在启动时是在一个 Goroutine 中去不断的取出 task 并执行之。因此,如下的调用方式在用户看来与直接通过 go func() 去执行一个 Goroutine 并没有什么区别。

gopool.Go(func(){  
/// do your job  
})  

内部实现

pool 的初始化

gopool包在被导入时,会在 init 方法中调用 NewPool 方法完成 defaultPool 全局协程池的初始化。 NewPool 会返回前文所说的 pool 对象,pool对象就是 Pool interface 的实现类型。

image.png

后续用户在执行诸如 Go 和 CtxGo 方法去注册任务时,其实就是通过 defaultPool 去调用 pool 对象的 CtxGo 方法。 Go 和 CtxGo 的区别是, CtxGo 会接收调用者传入的 context 上下文,以便调用者通过 context 向执行函数 f 传入参数或者随时停止执行函数 f。Go 方法就是直接调用 CtxGo 方法,区别只是在 Go 方法中会向 CtxGo 传递一个 context.Background。 image.png

task的注册

现在我们从 CtxGo 方法开始,看看 pool 怎么去注册 task 和启动 worker。 在 CtxGo 方法中,首先会执行 taskPool.Get 方法,取出一个 task。将这个 task 注册到 pool 对象的 task 链表的尾部。

taskPool 是一个 sync.Pool 的变量。使用 sync.Pool 去管理 task 对象的原因是,在高并发场景下,每次调用 CtxGo 都必须生成 task 对象。而 sync.Pool 会去管理并复用这些 task,这样一来,就很大程度上减少了 CPU(包括内存分配和GC)对 task 对象的管理开销。 同样的,由于 worker 是用来执行 task 的对象,也需要频繁的生成/销毁,因此gopool中同样也使用 sync.Pool 对worker进行管理。

image.png

在完成 task 的注册后,pool 会判断以下两个条件是否有一个为真:

  1. pool 中 task 的数量超过了启动阈值,并且 worker 的数量小于 pool 中的并发任务数量限制。

这个启动阈值会在 NewPool 方法中通过调用 NewConfig 来默认配置 image.png

  1. pool 中没有正在执行的 worker。 只要以上两个判断有一个为真,那么就会通过 workerPool.Get()方法,从 sync.Pool 中取出一个 worker,通过 worke 的 run 方法,来启动 worker。

image.png

task的执行

worker 在 run 方法中会启动一个 Goroutinue,在这个 Goroutinue 中,主要进行两项工作:

  1. 不断的循环获取 task 任务对象,并调用 t.f() 来执行 task 中注册的任务函数,在task执行完成后,通过 Recycle 来向 taskPool 对象中归还已经执行完成的 task。
  2. 如果 pool 的 task链表为空,则关闭 worker。 image.png

总结

总得来说,gopool 的实现相当的轻量,在实现简单的 worker 任务并发控制的基础上,通过 sync.Pool 对性能做了优化。其对 sync.Pool 的使用很具有参考意义。