ants源码剖析(一)

1,392 阅读8分钟

简介:ants是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。

GitHub地址:panjf2000/ants: 🐜🐜🐜 ants is a high-performance and low-cost goroutine pool in Go, inspired by fasthttp./ ants 是一个高性能且低损耗的 goroutine 池。 (github.com)

大致流程图

ants开源库流程图.png

主要概念

  1. spinLock:ants中使用的锁,是基于CAS机制和指数回避算法实现的一种自旋锁,且多次解锁不会引发panic。
  2. pool:goroutine池
  3. workerArray:pool池的worker队列,存放所有goWorker,goworker在workerArray中是有序的,按照
  4. goWorker:运行任务的实际执行者,它启动一个goroutine来接受任务并执行函数调用。
  5. 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。
注意

  1. 从workerArray中获取的worker是对之前worker的复用,也就是说不需要启动一个新的goroutine来执行task任务,只需要将任务送到该worker的任务执行队列worker.task即可,该worker一开始执行run所启动的goroutine会执行该任务。
  2. 从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版(郝林)