万字解析Golang Ant 协程池实现原理

Ant 是做什么的?

   我想大家在工作中,可能都会遇到 goroutine 滥用导致内存泄漏或系统资源耗尽的情况。goroutine 的滥用在大型项目中尤为突出,特别是当我们接手了许多层级传递下来的代码时,为了避免修改原有代码逻辑,我们可能会倾向于封装代码、使用 goroutine 进行后台处理。这种方式表面上看不会影响主分支逻辑,甚至能带来一定的性能提升,心想我真是个小天才。然而,随着时间的推移,越来越多的 goroutine 堆积起来,导致上下文切换越来越频繁,最终会导致内存占用增加。性能下降。

Golang 设计的 GMP 模型初衷就是为了减少上下文切换所带来的开销,GMP 模型由 Goroutine (G)、线程 (M) 和逻辑处理器 § 组成,其目的在于高效利用系统资源,实现轻量级的并发操作,减少创建线程和上下文切换的开销。然而,当我们无节制地创建 goroutine 时,这不仅违背了 Golang 团队开发 GMP 模型的初衷,还会引入严重的性能问题。

特别是在一些质量不太理想的分支代码中,可能没有传递 context 或者没有处理好 goroutine 的生命周期管理。这种情况下,如果 goroutine 长时间处于阻塞状态,例如 goroutine 向已满的 channel 发送数据而接收方迟迟没有读取,或者 goroutine 依赖于 channel 接收数据但发送方忘记关闭 channel,就会导致 goroutine 泄漏。这些问题如果没有及时发现,一旦系统中出现大量积累的goroutine ,可能导致系统资源耗尽甚至系统崩溃。

   到这里就会想,有没有一种工具能够管理goroutine,使其不会因为goroutine的滥用导致系统资源耗尽甚至崩溃,此时Ant就应运而生,ant翻译就是蚂蚁,蚂蚁是一个很神奇的种族,分工明确,虽然乱但是工作有序,就和创建这个项目一样,希望goroutine能够井然有序的完成工作,其目的就是能够管理goroutine的生命周期,避免因为goroutine的创建导致系统资源的消耗。

1. Ant设计思想和伪代码

   本部分就是先讲解下Ant的核心思想,然后对后续对核心思想进行扩展,平常我们执行goroutine时,只需要执行go func()即可运行所需要的函数,非常的简单粗暴,所以设计第一步就是要有一个能够执行用户自定义func的函数,但是怎么能够执行用户自定义的函数呢?没有什么问题是不能封装能解决的,于是就可以把go 关键字封装到goWorker中,使用回调函数可以把用户的函数传递进来,然后调用go 关键字运行,然后在丢给用户一个自己实现的方法不就行了?其中这个goroutine一定是在goWorker被关闭前,是一直运行的,用于执行用户自定义函数,这时如果能够对goWorker的数量进行判断,就能知道有多少个goroutine运行,从而进行限制,这不就能完成对协程数量的管理了吗?这个函数就叫submit,用于传递用户函数和判断goroutine的数量是否已经满了,但是如何能够及时的将用户自定义方法传递到goWorker中呢?当然是使用channel进行传递,基于上面的思想就可以大体知道ant的最核心思想了,下面是伪代码,用于快速理解。

type goWorker struct {
   task chan func() // task 的 channel
}
// 用于传递用户自定义的方法
func (p *Pool) Submit(task func()) error {
// 具体用于限制的goroutine的方法
    w := p.getWorker()
    if w != nil {
    w.task -< task
    }
}
func (w *goWorker) run() {
   	for f := range w.task {
   		if f == nil {
   			return
   		}
   		// 调用用户自定义函数
   		f()
   	}
   }()
}

2. Ant源码走读

2.1 goWorker

2.1 goWorker

   当我们在使用切片和map的时候,如果知道大概所需要的容量时,相信大家都会进行预分配,这样可以避免一次次扩容所造成的资源浪费,而这里也是ant也为用户提供了未预分配和预分配模式,他们分别对应两种方法堆栈和循环队列,ant默认的是堆栈模式,即初始化的时候,不会初始化任何的worker, 在第一次取得时候,肯定在stack取不到,就会在pool中,new一个插入到当前的栈当中,之后会通过栈中读取,废话不说直接看代码。

type goWorker struct {
	pool *Pool  // go worker 所属的工作池
	task chan func()  // 用于接受用户自定义的func
	lastUsed time.Time  // 当go work 被回收时会更新
}

   上面是worker 的核心,比上面我们的伪代码多了pool和lastUsed 字段,那么这两个字段是干什么的?pool 就是协程池,主要用于向用户提供各种api,以及完成各种判断,例如协程是否已经满了?都在这里进行,go work 就是一个勤勤恳恳的蚂蚁,不问东西,只完成自己工作,而pool可以类比成是一个庞大的蚁群,用于完成蚁群的使命(也就是用户所交代的task)。
   lastUsed 字段的作用是记录 goWorker 最后一次被使用的时间。当一个 goWorker 完成任务后,它会被放回到协程池的队列中,而 lastUsed 就会在这个时候更新为当前时间。这个字段是为了回收空闲的work。

func (w *goWorker) run() {
	w.pool.addRunning(1)
	go func() {
		defer func() {
			if w.pool.addRunning(-1) == 0 && w.pool.IsClosed() {
				w.pool.once.Do(func() {
					close(w.pool.allDone)
				})
			}
			w.pool.workerCache.Put(w)
			if p := recover(); p != nil {
				if ph := w.pool.options.PanicHandler; ph != nil {
					ph(p)
				} else {
					w.pool.options.Logger.Printf("worker exits from panic: %v\n%s\n", p, debug.Stack())
				}
			}
			// Call Signal() here in case there are goroutines waiting for available workers.
			w.pool.cond.Signal()
		}()

		for f := range w.task {
			if f == nil {
				return
			}
			f()
			if ok := w.pool.revertWorker(w); !ok {
				return
			}
		}
	}()
}

   其实这个方法在伪代码中已经说过了,这个协程会一直阻塞等待task,直到task进来之后,执行完用户自定义的func后,将当前的work进行回收,最后进行收尾,判断正在运行的work是否为0并对应的pool被关闭,则代表所有的任务都完成,然后将自己放入到sync.pool 中,这个pool其实就是一个对象池,复用对象可以减少GC,最后使用signal唤醒一个正在阻塞的协程。

2.2 goWorker

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Go 的学习之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值