golang 标榜轻量级协程。但是在大量创建协程后,调度性能以及GC的压力肯定会上升。那么在可预期的情况下我们可以选择控制并发数量(更推荐的做法). + 控制协程的数量。抱着传统的方式,当协程的数量过多,在创建与销毁之前占用了过多的时间的时候,我们通常就会考虑池化。通过空间换时间的方式来提升系统的性能。
在google 一番后发现没有特别官方的一个协程库。看到比较多的是ants协程库。于是准备学习分析一下看看其中精妙之处以及是否满足需要。
github: ants
获取 ants 代码
go get -u github.com/panjf2000/ants
作者在各大平台也有详细的介绍他写的这个库的目的以及相应的性能对比
知乎
可以看到作者是如何设计,以及源码的展示是非常好的一个学习机会。当然每个人的困惑的点是不一样的,之后带着困惑以及需求去看才能有提升。于是需要记录一下。
作为一个pool。最为基础的就是分为两部分,一部分是pool, 一部分就是worker级别的一个概念。多个worker组成了一个pool。pool作为一个集中管理器,而worker需要提供单个的功能以支持pool的一些功能。
源码如下:
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
}
}
}()
}
这里的主逻辑就是:
问题:
这边放回协程池中,但是整个协程并没有释放,所以其实这个goroutine是被block住了。再次调度那就是pool的事情了。
如何结束?
发送的func 为nil时,协程就会放到 sync.Pool中。
w.pool.workerCache.Put, w.pool.revertWorker的区别?为什么存在两个池子?
revertWorker 存放的是管理池子,.workerCache.Put 放的是sync.Pool。.workerCache.Put 在两次GC之间可以复用。revertWorker 相当于是不释放管理的池子,跨越了GC。
作为池子,最主要的功能就是在于管理和控制。这主要体现在几个方面。
结构体定于如下:
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{})
}
运行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 执行,或者达到最大数量阻塞的问题。
// 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
}
因为入队的时间是有序的,所以前面的时间会比后面的早。先获取最后一个需要释放的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()
}
}
以上就是目前比较通用的协程库的基本操作。但是可以看出这里有一些问题也还有一些优化点。