golang 协程池

golang协程池

golang 标榜轻量级协程。但是在大量创建协程后,调度性能以及GC的压力肯定会上升。那么在可预期的情况下我们可以选择控制并发数量(更推荐的做法). + 控制协程的数量。抱着传统的方式,当协程的数量过多,在创建与销毁之前占用了过多的时间的时候,我们通常就会考虑池化。通过空间换时间的方式来提升系统的性能。

在google 一番后发现没有特别官方的一个协程库。看到比较多的是ants协程库。于是准备学习分析一下看看其中精妙之处以及是否满足需要。

ants

github: ants

获取 ants 代码


go get -u github.com/panjf2000/ants

作者在各大平台也有详细的介绍他写的这个库的目的以及相应的性能对比
知乎

可以看到作者是如何设计,以及源码的展示是非常好的一个学习机会。当然每个人的困惑的点是不一样的,之后带着困惑以及需求去看才能有提升。于是需要记录一下。

组成

作为一个pool。最为基础的就是分为两部分,一部分是pool, 一部分就是worker级别的一个概念。多个worker组成了一个pool。pool作为一个集中管理器,而worker需要提供单个的功能以支持pool的一些功能。

worker

源码如下:

type Worker struct {
	// pool who owns this worker.
	pool *Pool

	// task is a job should be done.
	task chan func()

	// recycleTime will be update when putting a worker back into queue.
	recycleTime time.Time
}

// run starts a goroutine to repeat the process
// that performs the function calls.
func (w *Worker) run() {
	w.pool.incRunning()
	go func() {
	// 如果发生了panic 放到sync.Pool 中,来复用
		defer func() {
			if p := recover(); p != nil {
				w.pool.decRunning()
				w.pool.workerCache.Put(w)
				if w.pool.PanicHandler != nil {
					w.pool.PanicHandler(p)
				} else {
					log.Printf("worker exits from a panic: %v", p)
				}
			}
		}()
		
		// 单个的任务 每执行一次就会挂起,返回协程池中
		for f := range w.task {
		    // f 为nil 表示需要结束 这里是放回 sync.pool 中,以达到复用
			if nil == f {
				w.pool.decRunning()
				w.pool.workerCache.Put(w)
				return
			}
			f()
			// 执行完一次func 放回协程池
			if ok := w.pool.revertWorker(w); !ok {
				break
			}
		}
	}()
}

这里的主逻辑就是:

  1. 遍历task。获取task执行。执行完放回协程池中。

问题:

  1. 如何防止goroutine资源不被释放?

这边放回协程池中,但是整个协程并没有释放,所以其实这个goroutine是被block住了。再次调度那就是pool的事情了。

  1. 如何结束?
    发送的func 为nil时,协程就会放到 sync.Pool中。

  2. w.pool.workerCache.Put, w.pool.revertWorker的区别?为什么存在两个池子?

revertWorker 存放的是管理池子,.workerCache.Put 放的是sync.Pool。.workerCache.Put 在两次GC之间可以复用。revertWorker 相当于是不释放管理的池子,跨越了GC。

Pool

作为池子,最主要的功能就是在于管理和控制。这主要体现在几个方面。

  1. Pool 自身的属性,size,panic Handler,当前Pool大小等维护工作。
  2. 获取worker 执行func。
  3. 归还worker 放入池子。
  4. 定期清理闲置的worker 释放多余的协程,节省内存。

结构体定于如下:

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.
	expiryDuration time.Duration

	// workers is a slice that store the available workers.
	workers []*Worker

	// release is used to notice the pool to closed itself.
	release int32

	// lock for synchronous operation.
	lock sync.Mutex

	// cond for waiting to get a idle worker.
	cond *sync.Cond

	// once makes sure releasing this pool will just be done for one time.
	once sync.Once

	// workerCache speeds up the obtainment of the an usable worker in function:retrieveWorker.
	workerCache sync.Pool

	// PanicHandler is used to handle panics from each worker goroutine.
	// if nil, panics will be thrown out again from worker goroutines.
	PanicHandler func(interface{})
}

获取worker

运行func。

// retrieveWorker returns a available worker to run the tasks.
func (p *Pool) retrieveWorker() *Worker {
	var w *Worker

	p.lock.Lock()
	idleWorkers := p.workers
	n := len(idleWorkers) - 1
	// 取出一个worker
	if n >= 0 {
		w = idleWorkers[n]
		idleWorkers[n] = nil
		p.workers = idleWorkers[:n]
		p.lock.Unlock()
	// 没有idle 的 worker 从	cacheWorker 里面取
	} else if p.Running() < p.Cap() {
		p.lock.Unlock()
		if cacheWorker := p.workerCache.Get(); cacheWorker != nil {
			w = cacheWorker.(*Worker)
		} else {
			w = &Worker{
				pool: p,
				task: make(chan func(), workerChanCap),
			}
		}
		w.run()
	} else {
		// 已经满了 达到p 上限
		for {
			p.cond.Wait()
			l := len(p.workers) - 1
			if l < 0 {
				continue
			}
			w = p.workers[l]
			p.workers[l] = nil
			p.workers = p.workers[:l]
			break
		}
		p.lock.Unlock()
	}
	return w
}

整个过程就是获取worker 执行,或者达到最大数量阻塞的问题。

  1. 从idle池子中获取worker。
  2. 没有达到最大数量从 sync.Pool 中获取,这个动作介绍了申请内存,GC的东西,复用了存在的g。但是目测比绍少。
  3. 和系统申请一个新的goroutine运行。
  4. 已经达到了最大数量-> 阻塞,等待idle 池中有新的资源(个人认为此处有问题 如果是panic 资源释放到了sync.Pool 中 则改池子永远达不到要求)。

释放worker

  1. 设置空闲起始时间,用来判断空闲时间以释放资源。
  2. 放入到idle队列中。
// revertWorker puts a worker back into free pool, recycling the goroutines.
func (p *Pool) revertWorker(worker *Worker) bool {
	if CLOSED == atomic.LoadInt32(&p.release) {
		return false
	}
	worker.recycleTime = time.Now()
	p.lock.Lock()
	p.workers = append(p.workers, worker)
	// Notify the invoker stuck in 'retrieveWorker()' of there is an available worker in the worker queue.
	p.cond.Signal()
	p.lock.Unlock()
	return true
}

释放空闲期比较久的goroutine

因为入队的时间是有序的,所以前面的时间会比后面的早。先获取最后一个需要释放的worker。然后开始释放资源。释放的方式就是之前提到的worker判断了func是nil的话就会return。

// Clear expired workers periodically.
func (p *Pool) periodicallyPurge() {
	heartbeat := time.NewTicker(p.expiryDuration)
	defer heartbeat.Stop()

	for range heartbeat.C {
		if CLOSED == atomic.LoadInt32(&p.release) {
			break
		}
		currentTime := time.Now()
		p.lock.Lock()
		idleWorkers := p.workers
		n := -1
		// n 检查是否有idle 状态workers 被释放
		for i, w := range idleWorkers {
			if currentTime.Sub(w.recycleTime) <= p.expiryDuration {
				break
			}
			n = i
			w.task <- nil
			idleWorkers[i] = nil
		}
		if n > -1 {
			if n >= len(idleWorkers)-1 {
				p.workers = idleWorkers[:0]
			} else {
				p.workers = idleWorkers[n+1:]
			}
		}
		p.lock.Unlock()
	}
}

总结

以上就是目前比较通用的协程库的基本操作。但是可以看出这里有一些问题也还有一些优化点。

  1. 在revertWorker 的时候采取 p.cond.Signal() 是不够的。在panic 的时候同样应该做这个操作。并且在retrieveWorker 最后的地方应该是goto到最前面重新开始这段逻辑。可以参考runtime的goroutine 获取过程。
  2. func 队列可以增加缓存,让整个服务更加平缓。
  3. 增加单个func的超时时间。如果block住了 必须有一个可以退出的手段。

你可能感兴趣的:(go)