spinlock,自旋锁,乐观锁? Golang实现atomic.AddFloat64

在promethus包中使用了atomic这个包,如下面代码

110 func (g *gauge) Add(val float64) {
1         for {
2                 oldBits := atomic.LoadUint64(&g.valBits)
3                 newBits := math.Float64bits(math.Float64frombits(oldBits) + val)
4                 if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) {
5                         return
6                 }
7         }
8 }

之所以这么做是因为

为了达到这些要求,使用 取基值的bit大小 + CAS的方式来实现; 作者在视频中展示的benchmark的对比也可以看出 atomic的实现要快得多

类似的时序代码 #

a := int64(1)
basicA := a  // 相当于Load步骤,有多个goroutine同时获取到了相同的基数
b := a+int64(2) //新值大小

if atomic.CompareAndSwapInt64(&a, aabasicA, b) { //goroutine1
	fmt.Printf("good: %d\n", a)
}

//failed, because a isn't basicA anymore
if atomic.CompareAndSwapInt64(&a, basicA, b) { //goroutine2
	fmt.Printf("good: %d\n", a)
}

什么时候适合用spinlock #

怎么理解和正确使用spinlock #

其中要清楚一些概念:
增加大小增加到预定值,以及CAS,
相对应的为自定义的Addatomic.AddT(...)bool,以及 atomic.CompareAndSwapT(...)bool

增加大小增加到预定值以及CAS是不同的步骤,后两者是原子操作

增加大小的实现一关键步骤可以由 增加到预定值 或者 CAS完成

使用CAS 实现增加大小 #

Add操作要实现原子性,则在CAS失败要重新取基值,基于新的基值去CAS

load->CAS-done
load->CAS->failed->load(loop)

取完基值后,在交换时,可能交换失败(因为很可能在第一次取基值大小和第一次CAS之间基值发生了必变,所以CAS失败); 要重复 再取新的基值,再尝试交换;

使用CAS 实现增加大小的一个问题

有没有可能这样操作,两个goroutine同时把增加val从1增加为2,答案是不会

比如
oldBits  	 01111
--------------------------------------------------------------------------------
goroutine1 load:  01111
            goroutine2 load:  01111
//A:以上两步是原子性的,可能拿到要同的基值



goroutine1 math.Float64bits newBits:  01121
            goroutine2 math.Float64bits newBits:  01121
//B: 以上两步可能计算出相同的基值
--------------------------------------------------------------------------------



goroutine1 CAS newBits done:  01121
						goroutine2 CAS newBits done:  01121
//C:以上两步只可能成功一个

步骤C中只可能成功一个,因为CAS的返加 要么是成功要么失败,失败是因为swap的基数与传入oldValue不同导致不符合swap的前提(其中一个先执行了swap,valBits的值和另一个goroutine中的oldBits已经不一样了,导致另一个在进行原子操作swap oldBits与newBits时,发现valBits与oldBits不一样,返回false )

使用AddT 实现增加大小 #

使用AddUint64之类的AddT时可以这么做么? 不行,如下面会导致ranRet急速增长

for{
	expect:=atomic.LoadUint64(&ranRet)+ val //首先先计算 expect的值
	if newValue:=atomic.AddUint64(&ranRet, val);newValue==expect{ //原子操作
	break
	}
}

正确的做法,直接使用 atomic.AddUint64(&ranRet, val),不需要加loop for
而实现atomic.AddFloat64的要点是把float64转成bits用CAS实现,如promethus中的 math.Float64bits(math.Float64frombits(oldBits) + val)

参考资料:
prometheus/gauge.go
Bjorn Rabenstein - Prometheus: Designing and Implementing a Modern Monitoring Solution in G
github issue: For loop?

2021-07-04