Gopool 实现分析

Gopool 实现分析

Gopool 是字节跳动开源项目 gopkg 中的一个小工具,作用是以池化的思想来实现 goroutine 的复用,限制应用中 goutine 数量的无限增长。

使用介绍

Gopool 使用起来非常简单,只需要将项目中的 go 关键字替换为 gopool.Go:

go 复制代码
go func() {
	// do your job
}()

替换成

go 复制代码
gopool.Go(func(){
	/// do your job
})

文件目录

Gopool 目录下共有 6 个文件,非常简洁:

go 复制代码
.
├── README.md
├── config.go
├── gopool.go
├── pool.go
├── pool_test.go
└── worker.go

gopool

首先来看 gopool.go 这个文件。在这个文件的 init 方法中创建了 defaultPool:

scss 复制代码
func init() {
	defaultPool = NewPool("gopool.DefaultPool", math.MaxInt32, NewConfig())
}

示例中的 gopool.Go 实际上是在调用 defaultPool 的同名方法:

scss 复制代码
func Go(f func()) {
	CtxGo(context.Background(), f)
}

// CtxGo is preferred than Go.
func CtxGo(ctx context.Context, f func()) {
	defaultPool.CtxGo(ctx, f)
}

pool

defaultPool 实现了 Pool 接口,其定义位于 pool.go 中:

go 复制代码
type Pool interface {
	// 返回当前 pool 的名称
	Name() string
	// 设置 pool 中 goroutine 的最大数量
	SetCap(cap int32)
	// 执行用户传入的 func
	Go(f func())
	// 在用户指定的 context 中执行用户传入的 func
	CtxGo(ctx context.Context, f func())
	// 设置异常处理方法
	SetPanicHandler(f func(context.Context, interface{}))
	// 返回当前 running 状态的 worker
	WorkerCount() int32
}

同时在这个文件里定义了名为 pool 的结构体,并实现了上述 Pool 接口

go 复制代码
type pool struct {
	// 当前 pool 的名称
	name string

	// 当前 pool 的容量
	cap int32
	// 配置信息
	config *Config
	// 由 task 组成的链表
	taskHead  *task
	taskTail  *task
	taskLock  sync.Mutex
	taskCount int32

	// 记录当前处于 running 状态的 worker 数量
	workerCount int32

	// worker 异常时的恢复方法
	panicHandler func(context.Context, interface{})
}

让我们深入看一下 CtxGo 这个接口方法是怎么实现的:

css 复制代码
func (p *pool) CtxGo(ctx context.Context, f func()) {

   // 从 taskPool 中获取一个 task
	t := taskPool.Get().(*task)
	t.ctx = ctx
	t.f = f
	
	// 更新 taskHead 和 taskTail 两个指针的指向
	p.taskLock.Lock()
	if p.taskHead == nil {
		p.taskHead = t
		p.taskTail = t
	} else {
		p.taskTail.next = t
		p.taskTail = t
	}
	p.taskLock.Unlock()
	atomic.AddInt32(&p.taskCount, 1)
		
	// 如果 task 的数量比设置的阈值大,并且 p 的 worker 的数量小于 pool 的容量
	// 或者 p 的 worker 数量为 0
	if (atomic.LoadInt32(&p.taskCount) >= p.config.ScaleThreshold && p.WorkerCount() < atomic.LoadInt32(&p.cap)) || p.WorkerCount() == 0 {
	
	   //  从 workerPool 中获取一个 worker,并启动
		p.incWorkerCount()
		w := workerPool.Get().(*worker)
		w.pool = p
		w.run()
	}
}

taskPool 和 workerPool 是两个 sync.Pool 类型的全局对象,分别负责 task 和 worker 对象的复用。CtxGo 方法中首先会从 taskPool 的池子中取出一个 task 来复用,并调整 p 的 taskHead/taskTail/taskCount 等属性的值。在满足一些前提的情况下,还需要从 workerPool 中取出 worker,将 worker 的 pool 属性指向当前的 p,并调用 worker.run 方法。

task

task 是一个结构体,封装了用户传入的 func,同时还有一个指向下一个任务的属性

go 复制代码
type task struct {
	ctx context.Context
	f   func()

	next *task
}

func (t *task) zero() {
	t.ctx = nil
	t.f = nil
	t.next = nil
}

func (t *task) Recycle() {
	t.zero()
	taskPool.Put(t)
}

worker

worker 其实就是嵌入了一个 pool 的结构体:

go 复制代码
type worker struct {
	pool *pool
}

run 方法的实现如下:

scss 复制代码
func (w *worker) run() {
   // 启动一个 goroutine
	go func() {
	   // 在这个 goroutine 中,循环执行如下逻辑
		for {
		   // 从 w.pool 的队头取出一个 task 
			var t *task
			w.pool.taskLock.Lock()
			if w.pool.taskHead != nil {
				t = w.pool.taskHead
				w.pool.taskHead = w.pool.taskHead.next
				atomic.AddInt32(&w.pool.taskCount, -1)
			}
			if t == nil {
				// 如果 w.pool 的队头取不到 task 了,说明当前没有需要执行的 task 了,清理当前 worker
				w.close()
				w.pool.taskLock.Unlock()
				// 将当前 worker 放回 workerPool 中,供之后复用
				w.Recycle()
				return
			}
			w.pool.taskLock.Unlock()
			func() {
				defer func() {
				   // 如果在执行 t.f 的过程中发生了异常,进行异常恢复或记录现场
					if r := recover(); r != nil {
						if w.pool.panicHandler != nil {
							w.pool.panicHandler(t.ctx, r)
						} else {
							msg := fmt.Sprintf("GOPOOL: panic in pool: %s: %v: %s", w.pool.name, r, debug.Stack())
							logger.CtxErrorf(t.ctx, msg)
						}
					}
				}()
				
			   // 执行 task 上的 func
				t.f()
			}()
			
			// 将 t 放回到 taskPool 中,供之后复用
			t.Recycle()
		}
	}()
}

sync.Pool

频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺,而 sync.Pool 可以将暂时不用的对象缓存起来到一个池子中,待下次需要的时候直接使用,不用再次经过内存分配,可以减轻 GC 的压力,提升系统的性能。

使用 sync.Pool的步骤非常简单:

  1. 声明需要缓存对象的 new 方法
csharp 复制代码
var taskPool sync.Pool

func init() {
	taskPool.New = newTask
}

func newTask() interface{} {
	return &task{}
}
  1. 使用 Get 方法来从缓存池中获取一个对象
ini 复制代码
t := taskPool.Get().(*task)
t.ctx = ctx
t.f = f
  1. 对象不再使用时,放回到缓存池中
scss 复制代码
func (t *task) zero() {
	t.ctx = nil
	t.f = nil
	t.next = nil
}

func (t *task) Recycle() {
	t.zero()
	taskPool.Put(t)
}

总结

以上逻辑就是 gopool 实现 goroutine 复用的核心逻辑。我们来总结下它的原理:

  1. 当使用 gopool.Go 来执行用户传入的逻辑 func 时,这个 func 会被包装成为一个 task,添加到 defaultPool 的 task 链表中
  2. task 由 defaultPool 中的 worker 来实际负责执行。
  3. 当满足以下两种情况之一时,会新建一个 worker,并立即执行 worker 的调度逻辑:
  • task 的数量比设置的阈值大,并且 p 的 worker 的数量小于 pool 的容量
  • p 的 worker 数量为 0
  1. 不满足上述条件时,现有 worker 会按照 FIFO 顺序依次执行 task 链表中的 task。
  2. task 和 worker 实例都会通过 sync.Pool 来实现复用,减少内存分配,降低 GC 压力。
相关推荐
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
路在脚下@1 小时前
Spring Boot 的核心原理和工作机制
java·spring boot·后端
幸运小圣2 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
前端SkyRain3 小时前
后端Node学习项目-用户管理-增删改查
后端·学习·node.js
提笔惊蚂蚁3 小时前
结构化(经典)软件开发方法: 需求分析阶段+设计阶段
后端·学习·需求分析
老猿讲编程3 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
黄小耶@3 小时前
python如何使用Rabbitmq
分布式·后端·python·rabbitmq
宅小海4 小时前
Scala-List列表
开发语言·后端·scala
蔚一5 小时前
Javaweb—Ajax与jQuery请求
前端·javascript·后端·ajax·jquery