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

需要实现的方法也比较简单,基本就是原子操作的常规套路, getcompareAndSet,最初版本源码大概是这样:

//该函数获取第 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))

实际调用的代码就不给出了,大概就是存在多协程并发的调用 getcompareAndSet 方法。

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里面还是有一些经验可以总结:

  1. 确定好哪些对象是并发访问的,对于并发访问的对象一定要做好读写分析;
  2. 不止是对象的读写分析,还有对象里面的属性如果存在并发读写访问也要做并发控制;
  3. 对于 指针 类型的变量,涉及到 unsafe.Pointer 时候一定要谨慎,任何非类型安全的指针转换成(*Obj)都是读操作,谨慎谨慎。
  4. 不要用 uintptr 做指针变量的访问,uintptr只用作地址数学运算,并且都放在一行内执行,避免野指针。

以上是关于golang 记一次data race排查过程的主要内容,如果未能解决你的问题,请参考以下文章

golang data race 竞态条件

解Bug之路-记一次存储故障的排查过程

记一次 Flink 反压问题排查过程

记一次线上FGC问题排查

记一次ffmpeg进程阻塞的问题排查过程

记一次Kafka warning排查过程