简介:ants
是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。
大致流程图
主要概念
- spinLock:ants中使用的锁,是基于CAS机制和指数回避算法实现的一种自旋锁,且多次解锁不会引发panic。
- pool:goroutine池
- workerArray:pool池的worker队列,存放所有goWorker,goworker在workerArray中是有序的,按照
- goWorker:运行任务的实际执行者,它启动一个goroutine来接受任务并执行函数调用。
- task:执行的任务,是一个函数
每个pool有一个worker队列,存放着所有可用的worker,每个worker是对goWork的封装,goWork是实际执行运行任务的实例。
ants让每个worker执行尽量多的task,来减少goroutine的创建消耗(可以通过下面对worker.run()的剖析加深理解)
spinlock
ants中使用的锁,是基于CAS机制和指数回避算法实现的一种自旋锁。
type spinLock uint32 //实现sync.Locker接口
const maxBackoff = 16 //最大回避次数
func (sl *spinLock) Lock() {
backoff := 1
//尝试抢锁,如果本次没有抢到锁,使用指数回避算法来让自己在之后的某个时间段再随机抢锁(目的是减少下次抢锁失败的几率),一直自旋直到抢到锁
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
for i := 0; i < backoff; i++ {
//runtime.Gosched()函数功能:使当前goroutine让出CPU时间片(“回避”),让其他的goroutine获得执行的机会。当前的goroutine会在未来的某个时间点继续运行。
//注意:当一个goroutine发生阻塞,Go会自动地把与该goroutine处于同一系统线程的其他goroutines转移到另一个系统线程上去,以使这些goroutines不阻塞(从GMP模型角度来说,就是当与P绑定的M发生阻塞,P就与其解绑,然后与另一个空闲的M进行绑定 或者 去创建一个M进行绑定)。
runtime.Gosched()
}
if backoff < maxBackoff {//指数级上升,最大为16
backoff <<= 1
}
}
}
func (sl *spinLock) Unlock() {
//多次解锁不会panic,因为都只是使用原子操作把sl的值设为0
atomic.StoreUint32((*uint32)(sl), 0)
}
Pool
注意:在导包时,ants会自动初始化一个默认的Pool(在ants.go文件),其容量默认是math.MaxInt32
,其清道夫定期清理的时间间隔默认是1s
DefaultAntsPoolSize = math.MaxInt32
DefaultCleanIntervalTime = time.Second
// Init an instance pool when importing ants.
defaultAntsPool, _ = NewPool(DefaultAntsPoolSize)
ants现在有两种Pool,一种是Pool(在pool.go文件),一种是PoolWithFunc(在pool_func.go文件实现)。本篇只说Pool。
type Pool struct {
// an infinite pool is used to avoid potential issue of endless blocking caused by nested usage of a pool:
// submitting a task to pool which submits a new task to the same pool.
capacity int32 //如果为负值表示这是一个无限池
running int32 //池中正在运行的goroutine数量
lock sync.Locker //上面提到的spinlock锁,用于并发安全的从worker队列中获取空闲worker
workers workerArray //存放池中所有的worker,workerArray包含可用workers队列和过期workers队列,只会从可用workers队列中取可用worker
state int32 //池是否关闭的标志,为1表示池已关闭
cond *sync.Cond //条件原语,pool为阻塞模式时,如果retrieveWorker函数获取不到可用worker并且没有达到池的最大阻塞数量,会一直阻塞直到被唤醒
workerCache sync.Pool //临时对象池 用于在retrieveWorker函数中 加速获取一个可用的worker。
//retrieveWorker函数会在一些情况下(例如worker队列为空且池未满)通过临时对象池获取可用的worker
waiting int32 //当前被阻塞的goroutine数量
heartbeatDone int32 //为1表示已经停止清理过期worker,即结束运行 purgePeriodically
stopHeartbeat context.CancelFunc //用于通知 purgePeriodically 结束运行
options *Options //Pool的配置相关
}
type Options struct {//pool的配置, 源码使用了Option写法对配置进行可选初始化
ExpiryDuration time.Duration //清道夫定期清理过期worker的时间间隔
PreAlloc bool //初始化时是否内存预分配
MaxBlockingTasks int //pool.Submit被阻塞的最大goroutine数量,0表示没限制
Nonblocking bool //为true时 Pool.Submit 永远不会阻塞,且 MaxBlockingTasks 参数无效;
//为false时 表示是一个阻塞池,会根据 MaxBlockingTasks 参数设置进行阻塞
PanicHandler func(interface{}) //错误处理函数。worker发生panic时调用此函数,如果为nil则panic会继续向外层抛出
Logger Logger //日志记录器 可自定义,只需实现ants.go文件中的Logger接口即可。默认用官方logger
}
Pool中比较重要的函数
先来看NewPool()函数
func NewPool(size int, options ...Option) (*Pool, error) {
opts := loadOptions(options...)//加载配置
if size <= 0 {//如果容量为负数,置为-1,表示为无限池
size = -1
}
if expiry := opts.ExpiryDuration; expiry < 0 {//如果设置的清道夫定期清理时间间隔为负数则直接返回,并且提示对应错误
return nil, ErrInvalidPoolExpiry
} else if expiry == 0 {//如果没设置或者设置的值为0 就默认每隔1s清理一次过期worker
opts.ExpiryDuration = DefaultCleanIntervalTime
}
if opts.Logger == nil {//没有设置日志器,就默认用go官方的日志器
opts.Logger = defaultLogger
}
p := &Pool{
capacity: int32(size),
lock: internal.NewSpinLock(),
options: opts,
}
p.workerCache.New = func() interface{} {//设置临时对象池的New()函数
//调用临时对象池的Get()函数时会从临时对象池中拿一个worker出来
//如果临时对象池为空(比如gc刚释放了这部分内存),则会在内部调用这个New()函数生成一个新的worker对象返回
return &goWorker{
pool: p,
task: make(chan func(), workerChanCap),//workerChanCap是一个函数,根据GOMAXPROCS数来决定task是无缓冲还是有缓冲
}
}
if p.options.PreAlloc { //如果要内存预分配,就使用队列的方式实现WorkerArray接口,此情况下的workers队列容量最大为size
if size == -1 {
return nil, ErrInvalidPreAllocSize
}
p.workers = newWorkerArray(loopQueueType, size)//用预分配内存方式创建的pool不能通过Tune函数动态改变池的容量
} else { //否则用栈的实现方式(默认,workers队列容量无限制,可通过Tune动态改变)
p.workers = newWorkerArray(stackType, 0)
}
p.cond = sync.NewCond(p.lock)//初始化条件原语
var ctx context.Context
ctx, p.stopHeartbeat = context.WithCancel(context.Background())
go p.purgePeriodically(ctx)//用额外的协程去启动p.purgePeriodically(ctx),这是清理函数,用于定期检查释放池中过期的workers,ctx用于pool控制该goroutine什么时候结束
return p, nil
}
再来看创建pool时启动的purgePeriodically函数,俗称清道夫,用于定期清理池中过期的workers
func (p *Pool) purgePeriodically(ctx context.Context) {
heartbeat := time.NewTicker(p.options.ExpiryDuration) //定义一个断续器,根据配置的时间定期向通道发送清除信号
defer func() {
heartbeat.Stop()
atomic.StoreInt32(&p.heartbeatDone, 1)
}()
for {
select {
case <-heartbeat.C://接收到清除信号后去执行清理任务
case <-ctx.Done()://又或者ReleaseTimeout函数向该管道发送了信号,停止执行清理任务
return
}
//执行清理任务前 先检查池是否关闭
if p.IsClosed() {
break
}
p.lock.Lock()
expiredWorkers := p.workers.retrieveExpiry(p.options.ExpiryDuration) //获取池中过期的workers,回收时间在time.now()-p.options.ExpiryDuration之前的worker就是过期worker,后面会讲worker的这个函数
p.lock.Unlock()
for i := range expiredWorkers {
expiredWorkers[i].task <- nil //通知还在运行的过期worker 停止手上的工作
expiredWorkers[i] = nil //释放过期worker
}
// 因为有可能所有的worker都被清理了 或者 开发者调用了Tune函数扩大了pool的容量,但仍然有goroutine被p.cond.Wait()阻塞,此时就可唤醒全部goroutine去抢夺worker
if p.Running() == 0 || (p.Waiting() > 0 && p.Free() > 0) {
p.cond.Broadcast()
}
}
}
retrieveWorker函数返回一个可用的worker,如果pool为阻塞模式,则该函数可能会被阻塞。
retrieveWorker从pool的workerArray中 或 从pool的WorkerCache中 获取可用的worker。
注意:
- 从workerArray中获取的worker是对之前worker的复用,也就是说不需要启动一个新的goroutine来执行task任务,只需要将任务送到该worker的任务执行队列worker.task即可,该worker一开始执行run所启动的goroutine会执行该任务。
- 从WorkerCache中获取到的worker会在获取到时执行run方法又启动一个新的goroutine。从临时对象池中获取的worker不管是New方法新生成的还是某worker的run方法执行结束完后放到临时对象池的。
func (p *Pool) retrieveWorker() (w *goWorker) { //获取一个可用的worker
spawnWorker := func() {//定义一个 从临时对象池中获取一个新生成的worker 的函数
w = p.workerCache.Get().(*goWorker)
w.run()
}
p.lock.Lock()
w = p.workers.detach()
if w != nil { // first try to fetch the worker from the queue
p.lock.Unlock()
} else if capacity := p.Cap(); capacity == -1 || capacity > p.Running() { //如果工作队列为空 并且 池未满,就从临时对象池中获取
// if the worker queue is empty and we don't run out of the pool capacity,
// then just spawn a new worker goroutine.
p.lock.Unlock()
spawnWorker()
} else { // otherwise, we'll have to keep them blocked and wait for at least one worker to be put back into pool.
//这种情况只能阻塞直到获取一个可用的worker
if p.options.Nonblocking { //如果池是非阻塞模式,则直接返回nil切片
p.lock.Unlock()
return
}
retry: //如果池是阻塞模式
if p.options.MaxBlockingTasks != 0 && p.Waiting() >= p.options.MaxBlockingTasks { //如果当前阻塞goroutine数量 >= 设置的最大阻塞数,直接返回
p.lock.Unlock()
return
}
//否则阻塞直到收到通知有可用的worker
p.addWaiting(1)
p.cond.Wait() // block and wait for an available worker
p.addWaiting(-1)
if p.IsClosed() { //被唤醒后如果池已经关闭则直接结束
p.lock.Unlock()
return
}
var nw int
if nw = p.Running(); nw == 0 { // awakened by the scavenger
p.lock.Unlock()
spawnWorker() //如果是被清道夫唤醒则从 临时对象池中 获取worker
return
}
if w = p.workers.detach(); w == nil { //否则正常从worker队列中获取,如果没获取到
if nw < p.Cap() { //如果当前运行数量 小于 容量就从临时对象池获取
p.lock.Unlock()
spawnWorker()
return
}
goto retry //否则重试之前操作
}
p.lock.Unlock() //正常从worker队列中获取到了则返回
}
return
}
Submit会调用retrieveWorker函数获取一个可用的worker,获取到了worker就会将task发送到该worker的任务执行队列,等待worker执行。
func (p *Pool) Submit(task func()) error {
if p.IsClosed() {
return ErrPoolClosed
}
var w *goWorker
if w = p.retrieveWorker(); w == nil { //获取一个可用的worker
return ErrPoolOverload
}
w.task <- task //向worker中添加任务
return nil
}
revertWorker函数,把worker返还到pool中,更新worker回收时间。worker成功返还到pool为true ,否则false
func (p *Pool) revertWorker(worker *goWorker) bool {
if capacity := p.Cap(); (capacity > 0 && p.Running() > capacity) || p.IsClosed() { //如果当前运行的goroutine数量大于池的容量 或者 池已经关闭了 会操作失败
p.cond.Broadcast() //唤醒池中所有被阻塞的goroutine
return false
}
worker.recycleTime = time.Now() //否则更新此worker的回收时间
p.lock.Lock()
// To avoid memory leaks, add a double check in the lock scope.
// Issue: https://github.com/panjf2000/ants/issues/113
if p.IsClosed() { //双重检查 池是否关闭
p.lock.Unlock()
return false
}
err := p.workers.insert(worker) //将worker还给pool的可用worker队列
if err != nil { //如果队列满了或者容量为0 会还失败
p.lock.Unlock()
return false
}
// Notify the invoker stuck in 'retrieveWorker()' of there is an available worker in the worker queue.
p.cond.Signal() //成功还给池的可用worker队列后,唤醒一个被阻塞的goroutine去获取worker
p.lock.Unlock()
return true
}
Worker
根据pool的不同,ants也实现了两种worker,分别是goWorker和goWorkerWithFunc,pool底层worker用的是goWorker,而poolWithFunc底层使用的worker是goWorkerWithFunc。同上,本篇也只说Pool底层用的goWorker。
type goWorker struct { //运行任务的实际执行者,它启动一个goroutine来接受任务并执行函数调用。
// pool who owns this worker.
pool *Pool //表明这个goWorker是哪个池的
// task is a job should be done.
task chan func() //存放该goWorker要执行的所有工作,外部会通过调用pool.Submit方法向chan中发送任务
// recycleTime will be updated when putting a worker back into queue.
recycleTime time.Time //一个pool有一个worker队列,存储所有goWorker,每次取一个goWorker执行task,执行完放回worker队列,会更新goWork的此字段
}
goWorker的重要方法
run方法,执行后会启动一个goroutine,结束时会进行善后工作将worker放入到pool的临时对象池中,并且唤醒一个被阻塞的goroutine。worker每执行完一次任务后就会尝试将自己返还到pool中,使得又可以从Submit中接收到新的task任务执行,目的就是尽量让每个worker.run启动的goroutine能处理尽量多的任务,从而使得pool能以更少的goroutine执行更多的任务,从而达到更高的性能。
func (w *goWorker) run() {
w.pool.addRunning(1) //池中正在运行的goroutine数量+1
go func() {
defer func() { //善后工作
w.pool.addRunning(-1)
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 a panic: %v\n", p)
var buf [4096]byte
n := runtime.Stack(buf[:], false)
w.pool.options.Logger.Printf("worker exits from panic: %s\n", string(buf[:n]))
}
}
// Call Signal() here in case there are goroutines waiting for available workers.
w.pool.cond.Signal() //通知被阻塞的goroutine,有可用的worker了
}()
for f := range w.task { //如果没有任务则会一直阻塞
if f == nil { //nil为停止工作信号
return
}
f()
if ok := w.pool.revertWorker(w); !ok { //执行完一个任务后尝试将worker返回给池,来不断获取新任务执行,尽最大努力降低创建goroutine的消耗。返回失败表示 池已经关闭 或者 池中运行的goroutine达到了上限 或者 池的可用worker队列已满或者容量为0
return
}
}
}()
}
WorkerArray接口
WorkerArray存储了Pool中所有的Worker
在worker_array.go
文件中定义了workerArray的接口,定义如下:
type workerArray interface {
len() int //长度
isEmpty() bool
insert(worker *goWorker) error //往workerArray中存入worker
detach() *goWorker //从workerArray中获取可用worker
retrieveExpiry(duration time.Duration) []*goWorker //清道夫调用pool.worker中的此方法来清理pool.workers中的过期worker
reset() //清空workerArray中worker
}
该接口有两种实现方式:workerStack和loopQueue,具体实现分别在worker_stack.go和worker_loop_queue.go文件,通过工厂模式来根据不同需求创建不同的实例,默认是返回workerStack。
只有在创建pool时配置了进行内存预分配的选项才会创建loopQueue实例,且loopQueue的容量就是池的容量,并且创建之后该pool的workerArray(即loopQueue)的容量是固定的, pool的容量不能再通过Tune函数改变。如果非无限池(pool的capacity为负数表示是无限池)且创建的是workerStack实例则可通过Tune函数改变容量。
WorkerArray接口实现之WorkerStack
workerStack中底层数据结构使用的是栈,且其size属性没用,并且在调用其创建方法newWorkerStack的地方传的size值恒为0
type workerStack struct {
items []*goWorker //可用的worker
expiry []*goWorker //过期的goWorker
size int //workerStack中此属性没使用
}
workerStack重要方法实现之newWorkerStack实现
func newWorkerStack(size int) *workerStack { //注意:目前ants源码中所有调用此函数的地方传值恒为0
return &workerStack{
items: make([]*goWorker, 0, size),
size: size,
}
}
workerStack重要方法实现之insert实现
func (wq *workerStack) insert(worker *goWorker) error {
wq.items = append(wq.items, worker) //将元素插入栈顶
return nil
}
workerStack重要方法实现之detach实现
func (wq *workerStack) detach() *goWorker {
l := wq.len()
if l == 0 {
return nil
}
w := wq.items[l-1] //从栈顶取
wq.items[l-1] = nil // avoid memory leaks
wq.items = wq.items[:l-1]
return w
}
workerStack重要方法实现之retrieveExpiry实现
通过二分法查找workerStack的过期worker(worker.recycleTime < expiryTime就算过期)
1.计算过期时间expiryTime
2.通过二分查找法找到最后一个过期的worker,并且得到其在workerStack中的下标index
3.如果index为-1表示没有过期的worker,返还空切片。否则返还对应过期的worker切片
注意:为什么可以用二分?因为worker在workerStack是按照recycleTime从小到大有序存放的。
func (wq *workerStack) retrieveExpiry(duration time.Duration) []*goWorker {
n := wq.len()
if n == 0 {
return nil
}
expiryTime := time.Now().Add(-duration) //计算过期时间,<=expireTime的都属于过期
index := wq.binarySearch(0, n-1, expiryTime)
wq.expiry = wq.expiry[:0]
if index != -1 {
wq.expiry = append(wq.expiry, wq.items[:index+1]...)
m := copy(wq.items, wq.items[index+1:])
for i := m; i < n; i++ {
wq.items[i] = nil
}
wq.items = wq.items[:m]
}
return wq.expiry
}
func (wq *workerStack) binarySearch(l, r int, expiryTime time.Time) int {
var mid int
for l <= r {//栈中l是recycleTime最小的worker下标,r则是recycleTime最大的worker下标
mid = (l + r) / 2
if expiryTime.Before(wq.items[mid].recycleTime) {//如果expiryTime小于当前worker的recycleTime
r = mid - 1
} else {//否则
l = mid + 1
}
}
return r
}
workerStack重要方法实现之reset实现
func (wq *workerStack) reset() {
for i := 0; i < wq.len(); i++ {
wq.items[i].task <- nil//先向workerStack中所有worker发送终止运行任务的信号,worker运行完当前已有的任务后会结束对应的goroutine
wq.items[i] = nil//然后将workerStack此位置设为nil
}
wq.items = wq.items[:0]//然后清空workerStack中的所有worker
}
WorkerArray接口实现之loopQueue
loopQueue底层数据结构使用的是环形队列,当head==tail时通过isFull作为标志位来判断队列为空还是为满。
(额外加餐:环形队列还可以通过预留一个不用的空位置来判断队列是空还是满,当(tail+1)%len==head时即表示队列为满,当tail==head时表示队列为空)
type loopQueue struct {
items []*goWorker //存放可用worker 环形队列 尾部存,头部取
expiry []*goWorker //存放到期的worker
head int //队列头指针 取一个+1 达到size时置0
tail int //尾指针 存一个+1 达到size时置0
size int //队列最大容量
isFull bool //当head==tail时 通过此标志位判断队列为空还是满
}
loopQueue重要方法实现之newWorkerLoopQueue实现
func newWorkerLoopQueue(size int) *loopQueue {//用户指定size,ants保证在调用此函数时传入的size为正整数
return &loopQueue{
items: make([]*goWorker, size),
size: size,
}
}
loopQueue重要方法实现之insert实现
func (wq *loopQueue) insert(worker *goWorker) error {
if wq.size == 0 { //容量为0不能插入
return errQueueIsReleased
}
if wq.isFull { //队列满了也不能插入
return errQueueIsFull
}
wq.items[wq.tail] = worker //插入到队列尾部
wq.tail++ //尾指针后移
if wq.tail == wq.size { //维护环形队列
wq.tail = 0
}
if wq.tail == wq.head { //插入后tail==head只有队列满的情况才会发生
wq.isFull = true
}
return nil
}
loopQueue重要方法实现之detach实现
func (wq *loopQueue) detach() *goWorker { //从可用队列中取一个worker
if wq.isEmpty() {
return nil
}
w := wq.items[wq.head] //从头部取
wq.items[wq.head] = nil //防止内存泄漏 不用了就置为nil方便gc清理
wq.head++ //头指针后移
if wq.head == wq.size { //维护环形队列
wq.head = 0
}
wq.isFull = false //不管之前队列是否为满,执行一次detach之后一定为不满的状态
return w
}
loopQueue重要方法实现之retrieveExpiry实现
注意:这与前面栈的实现不同,因为队列的头指针不一定是0,所以二分查找需要进行映射处理
// 返回队列中到期的worker列表,并且把队列中过期的worker从可用worker列表中剔除,放入到过期队列中
func (wq *loopQueue) retrieveExpiry(duration time.Duration) []*goWorker {
expiryTime := time.Now().Add(-duration) //计算到期时间 到期时间之前回收的worker就算过期
index := wq.binarySearch(expiryTime) //队列worker是按照回收时间有序存放的,所以可通过二分法找到,index是最后一个过期worker的下标
if index == -1 { //没有过期的worker
return nil
}
wq.expiry = wq.expiry[:0] //置空
if wq.head <= index { //因为是环形队列,所以有两种情况
wq.expiry = append(wq.expiry, wq.items[wq.head:index+1]...)
for i := wq.head; i < index+1; i++ { //将队列中过期的worker置为nil
wq.items[i] = nil
}
} else { //如果index在head前面,则实际长度为 [0:index+1] + [head:]
wq.expiry = append(wq.expiry, wq.items[0:index+1]...)
wq.expiry = append(wq.expiry, wq.items[wq.head:]...)
for i := 0; i < index+1; i++ {
wq.items[i] = nil
}
for i := wq.head; i < wq.size; i++ {
wq.items[i] = nil
}
}
head := (index + 1) % wq.size //重新更新队列头指针
wq.head = head
if len(wq.expiry) > 0 { //不管之前队列是否为满,只要本次检查出的过期worker数量大于0,队列就是不满的状态
wq.isFull = false
}
return wq.expiry
}
func (wq *loopQueue) binarySearch(expiryTime time.Time) int { //二分查找
var mid, nlen, basel, tmid int
nlen = len(wq.items)
// if no need to remove work, return -1
if wq.isEmpty() || expiryTime.Before(wq.items[wq.head].recycleTime) { //如果队列为空 或者 队列所有的worker都没过期
return -1
}
// example
// size = 8, head = 7, tail = 4
// [ 2, 3, 4, 5, nil, nil, nil, 1] true position
// 0 1 2 3 4 5 6 7
// tail head
//
// 1 2 3 4 nil nil nil 0 mapped position
// r l
// base algorithm is a copy from worker_stack
// map head and tail to effective left and right
r := (wq.tail - 1 - wq.head + nlen) % nlen
basel = wq.head
l := 0
for l <= r {
mid = l + ((r - l) >> 1)
// calculate true mid position from mapped mid position
tmid = (mid + basel + nlen) % nlen
if expiryTime.Before(wq.items[tmid].recycleTime) {
r = mid - 1
} else {
l = mid + 1
}
}
// return true position from mapped position
return (r + basel + nlen) % nlen
}
相关资料
[1]二进制指数退避算法_百度百科
[2]指数退避算法 - itheone - 博客园 (cnblogs.com)
[3]Go并发编程实战 第2版(郝林)