sync/atomic不能直接原子替换链表节点指针,因为unsafe.Pointer需确保所指内存不被回收,否则引发UAF;Go缺乏hazard pointer等内存回收机制,必须配合标记-延迟释放或第三方库如cznic/b。
为什么 sync/atomic 不能直接原子替换链表节点指针
go 的 unsafe.pointer 本身不可被 atomic.storepointer 直接安全更新,除非你确保被替换的指针所指向的内存**在整个生命周期内不会被回收**。无锁链表最常踩的坑就是:旧节点刚被 cas 替换掉,还没来得及释放,另一个 goroutine 就通过旧指针访问了已释放/重用的内存 —— 这不是 data race,而是 uaf(use-after-free),go run -race 检不出,但会崩溃或静默错乱。
根本原因在于 Go 没有内置的 hazard pointer 或 epoch-based reclamation 机制。所以实际实现中,要么引入第三方库(如 github.com/cznic/b 中的 lfstack),要么自己做内存生命周期管理。
- 不要用
atomic.CompareAndSwapPointer(&head, old, new)直接替换,除非你能保证old指向的节点永远不被 GC 回收(比如全局池预分配 + 手动复用) - 插入/删除操作必须配套“标记-延迟释放”逻辑,典型做法是把待释放节点挂到本地 goroutine 的 defer 链或全局惰性回收队列
-
runtime.SetFinalizer不可靠,不能用于无锁结构的内存安全 —— 它触发时机不确定,且无法阻止 GC 提前回收指针可达对象
Insert 和 Delete 如何用 CAS 实现线性一致性
标准 Michael-Scott 无锁队列的思路可迁移到单向链表,但需注意 Go 的指针语义和 GC 行为。关键不是“能不能 CAS”,而是“CAS 成功后,其他 goroutine 是否还能看到一致的逻辑状态”。
以无头结点的无锁栈(LIFO 插入)为例:
type node struct {
value interface{}
next *node
}
func (s *stack) Push(v interface{}) {
n := &node{value: v}
for {
top := atomic.LoadPointer(&s.head)
n.next = (*node)(top)
if atomic.CompareAndSwapPointer(&s.head, top, unsafe.Pointer(n)) {
return
}
}
}
-
atomic.LoadPointer和atomic.CompareAndSwapPointer必须成对使用,且中间不能有 GC 触发导致n被提前回收(所以建议用对象池复用node) - 删除操作不能只靠 CAS,必须先用原子标记(如把
next置为特殊值unsafe.Pointer(uintptr(1)))表示“正在删除”,再 CAS 删除,否则会出现 ABA 问题 - Go 的
uintptr不是原子类型,别试图用atomic.Uintptr存裸地址 —— 必须走unsafe.Pointer+atomic系列函数
Go 标准库里其实没有生产可用的 Lock-Free Link 实现
sync.Map 不是无锁链表,它是分片哈希表;container/list 是互斥锁保护的双向链表;sync.Pool 内部用了无锁栈,但封装太深、不暴露接口,且仅用于对象复用,不能当通用链表用。
如果你真需要 Lock-Free Link,现实路径只有两条:
- 用
github.com/cznic/b:它提供了lfstack(无锁栈)和lfqueue(无锁队列),底层基于 hazard pointer 模拟,支持 Go 1.12+,经受过生产检验 - 自己实现时,必须搭配内存回收策略 —— 推荐用
runtime.GC()后的debug.FreeOSMemory()做兜底?不行,太重;更可行的是每个 P 维护一个本地待回收节点链表,每 N 次操作后批量释放 - 别忽略
GOEXPERIMENT=fieldtrack(Go 1.21+)—— 它能帮助检测某些指针逃逸模式,对调试无锁结构内存生命周期有间接帮助
测试无锁链表比写它还难
并发测试不是多开几个 goroutine 跑几轮就能覆盖的。真实问题往往出现在特定调度顺序下:比如两个 goroutine 同时 Insert,一个在 CAS 前被抢占,另一个完成插入并触发 GC,此时第一个 goroutine 的 n.next 指向已释放内存。
- 用
go test -race -count=100只能发现明显 data race,UAF 和 ABA 很难触发 - 推荐加
GOMAXPROCS=1强制串行调度 + 手动runtime.Gosched()插入断点,模拟临界时序 - 真正可靠的验证方式是形式化建模:用 TLA+ 写链表操作的原子步骤,再用 TLC 检查线性一致性 —— 工程成本高,但值得对核心模块做一次
无锁结构的“无锁”只是消除了互斥等待,没消除内存管理和时序依赖。越想绕过锁,越要花力气管内存、压时序、验模型。











