在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 }
之所以这么做是因为
- 使用的是float的类型的参数来达到 范型化(float即可兼容unsigned interger如counter,或者interger如gaugekk;w
- 在考虑性能提前下,不用mutex或者chan来实现增加大小功能
- atomic包里面没有
AddFloat64
可用
为了达到这些要求,使用 取基值的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 #
- lock的时间比较短op在ms以内,以取得比mutex更高的性能; 如果lock时间比较或者拿不准,请求使用mutext,能用RwMutext最好
- 大多处理器环境中
怎么理解和正确使用spinlock #
其中要清楚一些概念:
增加大小
与增加到预定值
,以及CAS
,
相对应的为自定义的Add
与 atomic.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?