Golang中用CAS实现无锁切片Append失败,求问题原因与正确方案
无锁并发切片Append的问题分析与修复
为什么使用append的版本会出现并发安全问题
Go语言中切片的append操作不是原子性的,且存在底层数组复用的特性:
- 当原切片的底层数组还有剩余容量(
cap)时,append会直接在原数组末尾添加元素,不会分配新数组。这意味着多个goroutine同时对同一个切片执行append时,会直接修改共享的底层数组,导致元素被覆盖、数据错乱。 - 即使
append触发了新数组分配,在你调用CompareAndSwap之前,其他goroutine可能已经修改了sync.Map中存储的切片指针,导致你基于旧切片生成的freshActions已经过时,最终CAS失败或者覆盖正确数据。
在你的错误实现中,freshActions := append(*oldActions, action)生成的新切片可能和*oldActions共享底层数组,当多个goroutine同时执行这一步时,会互相干扰,导致最终存储的切片元素缺失或重复,进而执行时sum计算错误。
正确的CAS实现无锁切片Append
要实现安全的无锁切片Append,必须保证每次更新都基于独立的底层数组,避免共享修改。核心思路是:
- 加载当前切片指针
- 创建一个全新的切片,长度为原切片长度+1,手动复制原切片的所有元素
- 添加新元素到新切片
- 使用
CompareAndSwap原子替换旧切片指针,失败则循环重试
正确实现代码如下:
func (group *Actions) Add(key string, action func()) { for { value, loaded := group.store.Load(key) if !loaded { actions := []func(){action} value, loaded = group.store.LoadOrStore(key, &actions) if !loaded { return } } oldActions := value.(*[]func()) // 创建新切片,独立底层数组 freshActions := make([]func(), len(*oldActions)+1) copy(freshActions, *oldActions) freshActions[len(*oldActions)] = action if group.store.CompareAndSwap(key, oldActions, &freshActions) { return } } }
关键改进点
- 使用
make创建固定长度的新切片,确保拥有独立的底层数组,彻底避免并发修改共享数组的问题 - 通过
copy复制原切片元素,保证新切片的初始状态是原切片的完整副本 - 循环重试
CompareAndSwap,直到原子替换成功,确保最终存储的切片是最新状态
这样修改后,sync.Map中存储的每个切片指针都对应独立的底层数组,CAS操作能保证每次更新都是原子性的,不会出现竞态条件,最终执行Exec时能得到正确的count和sum结果。
内容的提问来源于stack exchange,提问作者ksCaesar
相关产品推荐
相关产品推荐

