golang 记一次data race排查过程
Posted 惜暮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang 记一次data race排查过程相关的知识,希望对你有一定的参考价值。
golang 记一次data race排查过程
data race在写并发代码时候经常遇到,相关基础概念的介绍可以参考之前一篇文章:golang data race 竞态条件https://louyuting.blog.csdn.net/article/details/103606831
这篇文章主要是记录项目中一次 data race的排查过程。
背景
大概背景是这样的,最近项目中需要在golang中实现一个并发安全的list容器,list容器的长度不一定,通过在初始化的时候指定,容器必须要能够并发安全的更新容器内的每一个元素。经过调研,决定采用类似于 Java 中的 AtomicReferenceArray
的实现方案。关于AtomicReferenceArray
的实现原理这里就不细说了,网上很多博客介绍。
需求大概是这样,容器里面保存的是一个指针列表,每一个指针指向一个统计窗口的数据结构的地址。因为golang中的数组申明时候必须指定长度,不指定长度默认就是slice,所以也就是借助于slice来实现数组的目的。大概结构如下图:
里面的W0 - W(n-1) 都是指针,指针的size是8bytes(64位机器),指针分别指向一个实际的存储实体(用红色的圆来表示)。base指向数组的首地址,length表示数组的长度。
红色实体的数据结构大概是这样:
type bucket struct
startTime uint64
value interface
需要实现的方法也比较简单,基本就是原子操作的常规套路, get
和 compareAndSet
,最初版本源码大概是这样:
//该函数获取第 idx 个元素的地址
func (a *atomicPointerArray) offset(idx int) uintptr
......//省略
func (a *atomicPointerArray) get(idx int) *bucket
// data race point
bucketPtr := *(**bucket)(unsafe.Pointer(a.offset(idx)))
bucketPointer := unsafe.Pointer(bucketPtr)
return (*bucket)atomic.LoadPointer(&bucketPointer)
func (a *atomicPointerArray) compareAndSet(idx int, except, update *bucket) bool
offset := (**bucket)(unsafe.Pointer(a.offset(idx)))
bucketPointer := (*unsafe.Pointer)unsafe.Pointer(offset)
// data race point
return atomic.CompareAndSwapPointer(bucketPointer,unsafe.Pointer(except),unsafe.Pointer(update))
实际调用的代码就不给出了,大概就是存在多协程并发的调用 get
和 compareAndSet
方法。
data race 现场
如前面所描述,我用 go test -v -race ./...
来跑data race的时候就爆出了数据竞争,显示 get
函数的第一行和compareAndSet
函数的第三行存在data race。
这个问题很隐蔽…
get
函数的第一行:bucketPtr实际上第 idx 个元素,也就是一个 *bucket 指针,这里产生了一个对第idx个元素的读操作,但是这个读不是原子的,这里不仔细分析,很难分得清楚。
compareAndSet
函数的第三行:这里很简单,就是一个对第idx个元素写操作,写是原子的。
所以也就是多协程的一个并发读写问题。
解决思路
定位了问题解决起来就很简单了,把 get
函数的第一行的读取操作去掉也就OK了,顺便简化下代码:
func (a *atomicPointerArray) get(idx int) *bucket
return (*bucket)(atomic.LoadPointer((*unsafe.Pointer)(a.elementOffset(idx))))
func (a *atomicPointerArray) compareAndSet(idx int, except, update *bucket) bool
return atomic.CompareAndSwapPointer((*unsafe.Pointer)(aa.elementOffset(idx)), unsafe.Pointer(except), unsafe.Pointer(update))
这样就get
函数避免了一次对 *bucket 的读操作。
经验总结
从这个case里面还是有一些经验可以总结:
- 确定好哪些对象是并发访问的,对于并发访问的对象一定要做好读写分析;
- 不止是对象的读写分析,还有对象里面的属性如果存在并发读写访问也要做并发控制;
- 对于 指针 类型的变量,涉及到 unsafe.Pointer 时候一定要谨慎,任何非类型安全的指针转换成(*Obj)都是读操作,谨慎谨慎。
- 不要用 uintptr 做指针变量的访问,uintptr只用作地址数学运算,并且都放在一行内执行,避免野指针。
以上是关于golang 记一次data race排查过程的主要内容,如果未能解决你的问题,请参考以下文章