背景
最近在学习开源的协程池项目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,从而三方系统触发限流的报错。在遇到这种报错时,传统的重试策略是每隔一段时间重试一次。但由于是固定的时间重试一次,重试时又会有大量的请求在同一时刻涌入,会不断地造成限流。
如果在遇到限流错误的时候,通过指数退避算法进行重试,我们可以最大程度地避免再次限流。相比于固定时间重试,指数退避加入了时间放大性和随机性,从而变得更加“智能”。
可见,指数退避算法可以看做是一种用来降低并发冲突的思想,利用它可以解决这一类的问题。