直接用 atomic.CompareAndSwapPointer 实现无锁队列易出错,因其未解决ABA问题、内存重排和节点生命周期管理;需结合双重校验、版本号或 runtime.KeepAlive 等机制才能保证线性一致性和内存安全。

为什么直接用 atomic.CompareAndSwapPointer 实现无锁队列容易出错?
因为无锁队列不是“把 push/pop 改成原子操作”就能跑通——它必须解决 ABA 问题、内存重排、节点生命周期管理这三座大山。很多初学者用 atomic.LoadPointer + atomic.CompareAndSwapPointer 模拟链表头尾指针,结果在高并发下出现丢数据、死循环或 panic,根本原因是没处理好指针悬空和重排边界。
入队(enqueue)必须保证尾节点追加的线性一致性
标准无锁单生产者/多消费者(SPMC)队列常用 Michael-Scott 算法,核心是维护 tail 和 next 字段的双重校验。真实场景中不能只更新 tail,而要先尝试插入新节点到当前尾节点的 next,再 CAS 更新 tail:
// Node 定义需含 next *Node 和 data interface{}
func (q *LockFreeQueue) Enqueue(v interface{}) {
newNode := &Node{data: v}
for {
tail := atomic.LoadPointer(&q.tail)
next := atomic.LoadPointer(&(*Node)(tail).next)
if tail == atomic.LoadPointer(&q.tail) { // double-check tail hasn't moved
if next == nil {
// 尝试将新节点挂到 tail.next
if atomic.CompareAndSwapPointer(&(*Node)(tail).next, nil, unsafe.Pointer(newNode)) {
// 成功:再尝试推进 tail
atomic.CompareAndSwapPointer(&q.tail, tail, unsafe.Pointer(newNode))
return
}
} else {
// tail 不是真尾,推进 tail 指针
atomic.CompareAndSwapPointer(&q.tail, tail, next)
}
}
}
}
- 必须用
unsafe.Pointer转换,Go 中原子指针操作只接受unsafe.Pointer - 两次
atomic.LoadPointer之间没有同步屏障,所以需要 double-check(即第二次读q.tail确认没变) - 如果
(*Node)(tail).next已被其他 goroutine 设置,说明 tail 滞后,应主动推进tail,否则会卡住
出队(dequeue)必须避免 ABA 问题导致的节点误回收
出队时若仅靠 head 指针 CAS 移动,可能遇到:A → B → C,线程1读到 head=A,准备 CAS 到 B;此时线程2出队 B,再入队新节点复用地址 B;线程1 CAS 成功但实际拿到的是新节点,数据错乱。解决方案是引入版本号或使用 runtime.KeepAlive 延迟回收:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
func (q *LockFreeQueue) Dequeue() (interface{}, bool) {
for {
head := atomic.LoadPointer(&q.head)
tail := atomic.LoadPointer(&q.tail)
headNode := (*Node)(head)
next := atomic.LoadPointer(&headNode.next)
if head == atomic.LoadPointer(&q.head) {
if head == tail {
if next == nil {
return nil, false // empty
}
// tail 已推进但 head 没跟上,帮助推进 head
atomic.CompareAndSwapPointer(&q.head, head, next)
} else {
val := (*Node)(next).data
if atomic.CompareAndSwapPointer(&q.head, head, next) {
runtime.KeepAlive(headNode) // 防止 headNode 过早被 GC 回收
return val, true
}
}
}
}
}
-
runtime.KeepAlive(headNode)是关键——它告诉编译器:该对象在这一行之后仍被逻辑使用,禁止提前释放内存 - 不能直接
free或置nil,Go 没有显式内存管理,依赖 GC,但 GC 不知道你是否还在用那个*Node地址 - ABA 在 Go 中虽不如 C/C++ 严重(无 malloc/free 手动复用),但对象池(
sync.Pool)或复用结构体时仍可能发生
真正能落地的建议:别手写,优先用 github.com/cznic/sorty 或 go.uber.org/atomic 的封装
自己实现无锁队列在 Go 中性价比极低:GC 干预、逃逸分析、unsafe 使用限制、测试难度高。实测表明,对大多数业务场景(QPS sync.Mutex 包裹的 slice 队列比手写无锁更快更稳——因为 Go runtime 对锁做了大量优化,且无锁代码极易因一个 atomic 顺序搞错就引发偶发 crash。
立即学习“go语言免费学习笔记(深入)”;
- 若真需高性能,用
chan(底层已做无锁优化)或golang.org/x/exp/concurrent(实验包里的Queue) - 所有涉及
unsafe.Pointer和atomic的跨字段操作,必须配//go:linkname或go:build gcflags测试内存模型行为 - 哪怕抄 Michael-Scott 论文伪代码,也要注意 Go 的
struct字段对齐会让next偏移量和 C 版本不同
无锁的复杂点不在原子指令本身,而在如何让多个 goroutine 对同一组内存位置达成一致的观察顺序——这点连 runtime 作者都在反复修 bug。

















