最近在学习一些优秀的go开源项目,浏览高性能协程池库ants的源码时,发现其自旋锁spinlock的实现颇有意思
在追求高性能的场景下,重量级锁带来的性能损耗通常来说是不可接受的,其常见的一种解决方案就是采用无锁化技术:自旋锁
注: 本文不会介绍CAS的实现思想和使用方式,建议同学提前了解~
话不多说,show code
如下代码就是ants中自旋锁spinlock的实现
其中spinLock的Lock方法就是尝试自旋加锁,可以看到其底层的CAS操作是基于go原生原子包atomic中提供的CAS方法,atomic.CompareAndSwapUint32
type spinLock uint32
const maxBackoff = 16
func (sl *spinLock) Lock() {
backoff := 1
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
// backoff是呈指数级增长的
for i := 0; i < backoff; i++ {
// 让出当前CPU时间片
runtime.Gosched()
}
if backoff < maxBackoff {
// 位运算,左移一位:1 -> 2 -> 4 -> 8 -> ···
backoff <<= 1
}
}
}
func (sl *spinLock) Unlock() {
atomic.StoreUint32((*uint32)(sl), 0)
}
func NewSpinLock() sync.Locker {
return new(spinLock)
}
现在,我们来计算最多可能的自旋次数,以及 runtime.Gosched() 被调用的总次数:
- 第一次失败:
backoff = 1,调用runtime.Gosched()1 次。 - 第二次失败:
backoff = 2,调用runtime.Gosched()2 次。 - 第三次失败:
backoff = 4,调用runtime.Gosched()4 次。 - 第四次失败:
backoff = 8,调用runtime.Gosched()8 次。 - 第五次失败:
backoff = 16(达到maxBackoff),调用runtime.Gosched()16 次
由此可知,单个groutine最多只会尝试5次获取自旋锁;且每次获取失败,退避值backoff是呈指数级增长的,实际的退避操作是循环backoff次调用runtime.Gosched()让出时间片,减少该groutine获取锁的频率
指数退避很好的解决了在锁竞争激烈的情况下对CPU的压力,防止大量的groutine持续高频地获取锁
指数退避在很多技术实现思想里都有出现,例如
- TCP协议中的拥塞避免
- 消息队列中消息处理失败的重试策略
- 数据库连接池连接失败的重试策略
还有诸多优秀的实现案例,感兴趣的同学们可以继续深入扩展了解