自旋锁之指数退避算法

299 阅读4分钟

背景

最近在学习开源的协程池项目ant,项目中用到了锁,作者觉得使用sync.mutex过于重,因此自己实现了一个很巧妙、轻量级的自旋锁,就是使用了指数退避算法来实现的,今天就来一起学习下指数退避算法!

自旋锁

这里就不过多解释锁的概念了,所谓的自旋锁,就是尝试获取锁的线程没拿到锁就一直死循环尝试去多次拿锁,看起来像是在原地打转,因此叫自旋锁。自旋锁的实现方式很多,本文讲一下如何利用指数退避算法实现。

指数退避算法

Exponential backoff is an algorithm that uses feedback to multiplicatively decrease the rate of some process, in order to gradually find an acceptable rate.

指数退避算法,就是使用反来成倍地降低某些过程的速率的算法,以便逐渐找到可接受的速率。直白来说,就是能够动态改变执行的速度,来降低竞争者们产生连续冲突的可能。

指数退避算法应用广泛,在以太网中,该算法通常用于冲突后的调度重传,以环节网络拥挤。

他还适用于网络应用的标准错误处理策略,使用这种策略,客户端会定期重试失败的请求。

比如,三方支付机构接入接口规范中,服务方交易结束结果通知商户都用到重发机制,通知频率为 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h

在app应用中,很多场景会遇到轮询一类的问题,一般的轮询对于app性能和电量的消耗都是个巨大的灾难。那如何解决这种问题呢?app在上一次更新操作之后还未被使用的情况下,使用指数退避算法来减少更新频率,从而节省资源和减少电的消耗。

指数退避算法实现自旋锁

自旋锁一个最大的诟病就是高频次的获取锁引发CPU空转,浪费性能。而从上边指数退避算法的应用场景可以看出,它可以在并发竞争中,协调冲突问题,这不正好可以应用于实现自旋锁么?利用它缓解多个线程竞争锁的内耗,可以有效避免自旋锁空转问题。具体如何实现,接口ant中spinLock来看一下:

// https://github.com/panjf2000/ants/tree/dev/internal/syncspinlock.go
package sync

import (
   "runtime"
   "sync"
   "sync/atomic"
)

//使用一个unit32来表示锁,0表示锁空闲,1表示被占用
type spinLock uint32

//设置了一个最大退让阈值
const maxBackoff = 16

func (sl *spinLock) Lock() {
   backoff := 1
   for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
		//获取失败后,退避让出cpu时间片
      for i := 0; i < backoff; i++ {
         runtime.Gosched()// Gosched 暂停当前goroutine,使其他goroutine先行运算
      }
      if backoff < maxBackoff {
         backoff <<= 1
      }
   }
}

func (sl *spinLock) Unlock() {
   atomic.StoreUint32((*uint32)(sl), 0)
}

// NewSpinLock instantiates a spin-lock.
func NewSpinLock() sync.Locker {
   return new(spinLock)
}

定义了一个uint32类型的锁spinLock,0表示锁空闲,1表示被占用;

常量maxBackoff作为退避次数的阈值,超过这个阈值,退避次数就不在增加。

核心逻辑在获取锁的Lock()方法中,作者使用atomic.CompareAndSwapUint32() 来控制并发获取锁,这个原子操作来确保只有一个协程可以拿到锁,拿锁失败的协程则进入退避流程进行等待重试,所谓退避就是调用runtime.Gosched(),当前协程让出CPU时间片让其他协程获得机会去竞争锁。

退避其他协程的次数由变量backoff控制,可以看到,每次竞争锁失败,当前协程退让次数都会翻倍(在未达到退避次数的阈值前),即随着竞争锁失败轮次增加,每一轮的退避次数依次为1,2,4,8,16,16... 直至成功获得锁。

用这种避退方式,把锁竞争的并发协程数量降下来,减少修改锁的冲突,从而达到提高锁新性能的目的。

拓展

指数退避算法除了以上应用外,业务开发过程中还有不少应用,一个典型的场景:

不可避免的要调用外部系统的API接口,某些时候大批量调用 API,从而三方系统触发限流的报错。在遇到这种报错时,传统的重试策略是每隔一段时间重试一次。但由于是固定的时间重试一次,重试时又会有大量的请求在同一时刻涌入,会不断地造成限流。

如果在遇到限流错误的时候,通过指数退避算法进行重试,我们可以最大程度地避免再次限流。相比于固定时间重试,指数退避加入了时间放大性和随机性,从而变得更加“智能”。

可见,指数退避算法可以看做是一种用来降低并发冲突的思想,利用它可以解决这一类的问题。