协程池的设计
前言:
尽管go官方宣称用golang写并发程序的时候随便起个成千上万的goroutine毫无压力;但是每个协程都会分配最小2k的内存,根据需要进行扩容,最大1G,哪怕最小的2k,当协程数达到一定数量时,内存会暴涨,所带来的就是gc的压力,频繁地进行gc也会给性能带来影响;此外内存暴涨,造成无内存可用时,Go调度器就会阻塞goroutine,结果就是P(GMP调度模型中的Processor逻辑处理器)的Local队列积压,又导致内存溢出,这就是个死循环…,甚至极有可能程序直接Crash掉;所以在大规模需要并发操作时,协程池是非常有必要的。
协程池的设计思路
初始化一个pool,这个pool中需要维护一个类似栈的LIFO队列 ,用来存放空闲的worker,,每个worker,接收一个tast进行协程操作,此外给每个worker设置了老化时间,方便定期回收过期的worker,通过定时清理过期worker,可以进一步节省系统资源;对于没有过期的worker会回收复用,因为复用,规避了无脑启动大规模goroutine的弊端,可以节省大量的内存。
当一个client端提交task到Pool中之后,在Pool内部,接收task之后的核心操作是:
- 检查空闲worker队列中是否有可用的worker,有的话,拿出最近执行任务的那个worker,即从队列的末尾拿最后一个
- 当没有空闲的worker时,判断当前正在running的worker数量是否已经达到pool容量,没有的话,新建一个新的worker;已经达到的话阻塞等待,等待一个新的worker被回收;
- 每个worker完成任务后,回收到空闲的worker队列中,并更新入队时间,按照执行时间的先后,放入到队列中;
- 定时任务来扫描空闲的worker队列中的最近的完成任务时间与当前时间之差是否大于定义的老化时间,是的话,释放掉该worker,节省内存资源;
代码
任务方法和信号
//空结构体不占内存,用于信号传递
type sig struct{}
type f func() error
pool结构体定义
// Pool accept the tasks from client,it limits the total
// of goroutines to a given number by recycling goroutines.
type Pool struct {
// capacity of the pool.
capacity int32
// running is the number of the currently running goroutines.
running int32
// expiryDuration set the expired time (second) of every worker.
//在空闲队列中的worker的最新一次运行时间与当前时间之差如果大于这个值则表示已过期,定时清理任务会清理掉这个worker
expiryDuration time.Duration
// freeSignal is used to notice pool there are available
// workers which can be sent to work.
//freeSignal是一个信号,因为Pool开启的worker数量有上限,因此当全部worker都在执行任务的时候,新进来的请求就需要阻塞等待,那当执行完任务的
//worker被放回Pool之时,如何通知阻塞的请求绑定一个空闲的worker运行呢?freeSignal就是来做这个事情的
freeSignal chan sig
// workers is a slice that store the available workers.
workers []*Worker
// release is used to notice the pool to closed itself.
release chan sig
// lock for synchronous operation
lock sync.Mutex
once sync.Once
}
worker结构体定义
// Worker is the actual executor who runs the tasks,
// it starts a goroutine that accepts tasks and
// performs function calls.
type Worker struct {
// pool who owns this worker.
pool *Pool
// task is a job should be done.
task chan f
// recycleTime will be update when putting a worker back into queue.
recycleTime time.Time
}
初始化pool
// NewPool generates a instance of ants pool
func NewPool(size, expiry int) (*Pool, error) {
if size <= 0 {
return nil, ErrPoolSizeInvalid
}
p := &Pool{
capacity: int32(size),
freeSignal: make(chan sig, math.MaxInt32),
release: make(chan sig, 1),
expiryDuration: time.Duration(expiry) * time.Second,
}
// 启动定期清理过期worker任务,独立goroutine运行,
// 进一步节省系统资源
p.monitorAndClear()
return p, nil
}
清理空闲的worker协程
//定时清理过期的worker
func (p *Pool) monitorAndClear() {
go func() {
for {
// 周期性循环检查过期worker并清理
time.Sleep(p.expiryDuration)
currentTime := time.Now()
p.lock.Lock()
idleWorkers := p.workers
n := 0
for i, w := range idleWorkers {
// 计算当前时间减去该worker的最后运行时间之差是否符合过期时长
if currentTime.Sub(w.recycleTime) <= p.expiryDuration {
//因为采用了LIFO后进先出队列存放空闲worker,所以该队列默认已经是按照worker的最后运行时间由远及近排序
break
}
n = i
w.stop()
//指针指向nil,主动释放堆中资源,不用go的gc来回收,减轻gc的压力
idleWorkers[i] = nil
p.running--
}
//重新将空闲的worker放入到pool的worker队列中
if n > 0 {
n++
p.workers = idleWorkers[n:]
}
p.lock.Unlock()
}
}()
}
// stop this worker.
func (w *Worker) stop() {
w.sendTask(nil)
}
提交任务
// Submit submit a task to pool
func (p *Pool) Submit(task f) error {
if len(p.release) > 0 {
return ErrPoolClosed
}
w := p.getWorker()
w.sendTask(task)
return nil
}
// sendTask sends a task to this worker.
func (w *Worker) sendTask(task f) {
w.task <- task
}
获取空闲的worker
// getWorker returns a available worker to run the tasks.
func (p *Pool) getWorker() *Worker {
var w *Worker
// 标志,表示当前运行的worker数量是否已达容量上限
waiting := false
// 涉及从workers队列取可用worker,需要加锁
p.lock.Lock()
workers := p.workers
n := len(workers) - 1
// 当前worker队列为空(无空闲worker)
if n < 0 {
// 运行worker数目已达到该Pool的容量上限,置等待标志
if p.running >= p.capacity {
waiting = true
} else {
// 否则,运行数目加1
p.running++
}
} else {
// 有空闲worker,从队列尾部取出一个使用
<-p.freeSignal
w = workers[n]
//主动回收堆中的资源
workers[n] = nil
p.workers = workers[:n]
}
// 判断是否有worker可用结束,解锁
p.lock.Unlock()
if waiting {
// 阻塞等待直到有空闲worker
<-p.freeSignal
p.lock.Lock()
workers = p.workers
l := len(workers) - 1
w = workers[l]
workers[l] = nil
p.workers = workers[:l]
p.lock.Unlock()
} else if w == nil {
// 当前无空闲worker但是Pool还没有满,
// 则可以直接新开一个worker执行任务
w = &Worker{
pool: p,
task: make(chan f),
}
w.run()
}
return w
}
worker的run方法
func (w *Worker) run() {
//atomic.AddInt32(&w.pool.running, 1)
go func() {
//监听任务列表,一旦有任务立马取出运行
for f := range w.task {
if f == nil {
atomic.AddInt32(&w.pool.running, -1)
return
}
f()
//回收复用
w.pool.putWorker(w)
}
}()
}
回收worker
// putWorker puts a worker back into free pool, recycling the goroutines.
func (p *Pool) putWorker(worker *Worker) {
// 写入回收时间,亦即该worker的最后运行时间
worker.recycleTime = time.Now()
p.lock.Lock()
p.workers = append(p.workers, worker)
p.lock.Unlock()
p.freeSignal <- sig{}
}
pool扩缩容
func (p *Pool) Cap() int {
return int(p.capacity)
}
// ReSize change the capacity of this pool
func (p *Pool) ReSize(size int) {
if size < p.Cap() {
diff := p.Cap() - size
for i := 0; i < diff; i++ {
p.getWorker().stop()
}
} else if size == p.Cap() {
return
}
atomic.StoreInt32(&p.capacity, int32(size))
}
协程池的包推荐:github.com/panjf2000/ants/v2