
在实现高性能、无锁(lock-free)并发数据结构时,例如基于maged m. michael和michael l. scott算法的非阻塞队列,经常需要对包含多个字段(如指针和计数器)的复合类型执行原子比较与交换操作。例如,一个常见的pointer_t结构体可能定义如下:
type node_t struct {
value interface{}
next pointer_t
}
type pointer_t struct {
ptr *node_t // 指向下一个节点的指针
count uint // 版本计数器或标记位
}当尝试对pointer_t类型的变量进行类似伪代码中的CAS(&tail.ptr->next, next, <node, next.count+1>)操作时,Go的sync/atomic包(如atomic.CompareAndSwapPointer或atomic.CompareAndSwapUint64)无法直接处理整个pointer_t结构体,因为这些操作通常仅限于单个机器字(如uintptr或uint64)。
位窃取是一种利用硬件特性,将额外信息编码到现有指针中的技术。在64位系统中,内存地址通常只需要48位或52位,这意味着指针的高位或低位可能存在未使用的比特位。这些未使用的比特位可以被“窃取”来存储一个小的整数(如版本计数器或删除标记),从而将一个结构体(指针+小整数)压缩成一个单字大小的值,然后就可以使用atomic.CompareAndSwapPointer进行原子操作。
const (
// 假设我们使用指针的最低3位存储一个计数器
// 实际应用中需要考虑内存对齐,确保这些位不会被真实地址使用
counterMask = 0x7 // 0b111
ptrMask = ^counterMask
)
// PackPointerAndCount 将指针和计数器编码为一个uintptr
func PackPointerAndCount(ptr *node_t, count uint) uintptr {
// 确保计数器不会溢出可用位数
if count > counterMask {
panic("count exceeds available bits")
}
return (uintptr(unsafe.Pointer(ptr)) & ptrMask) | uintptr(count)
}
// UnpackPointerAndCount 从uintptr中解码出指针和计数器
func UnpackPointerAndCount(packed uintptr) (*node_t, uint) {
ptr := (*node_t)(unsafe.Pointer(packed & ptrMask))
count := uint(packed & counterMask)
return ptr, count
}
// 假设我们有一个需要原子更新的packedValue
var atomicPackedValue uintptr
func updateNodeAndCount(oldPacked uintptr, newNode *node_t, newCount uint) bool {
newPacked := PackPointerAndCount(newNode, newCount)
return atomic.CompareAndSwapUintptr(&atomicPackedValue, oldPacked, newPacked)
}写时复制是一种更通用、更安全的方法,适用于需要原子更新任意大小结构体的场景。其核心思想是:将要更新的结构体视为不可变的。当需要修改结构体时,不是直接修改原结构体,而是创建一个原结构体的副本,修改这个副本,然后原子地将指向原结构体的指针替换为指向新副本的指针。
结构体修改: 将需要原子更新的结构体(例如pointer_t)本身作为指针的目标,即node_t中的next字段不再是pointer_t类型,而是*pointer_t类型。
立即学习“go语言免费学习笔记(深入)”;
type node_t struct {
value interface{}
next *pointer_t // 改变为指针类型
}
type pointer_t struct {
ptr *node_t
count uint
}更新操作:
import (
"sync/atomic"
"unsafe"
)
type node_t struct {
value interface{}
next *pointer_t // next 字段现在是一个指针
}
type pointer_t struct {
ptr *node_t
count uint
}
// UpdateNextPointer 原子地更新 node_t 的 next 字段
func UpdateNextPointer(node *node_t, oldPointer *pointer_t, newNode *node_t, newCount uint) bool {
// 1. 创建新的 pointer_t 结构体
newPointer := &pointer_t{
ptr: newNode,
count: newCount,
}
// 2. 使用 atomic.CompareAndSwapPointer 替换指针
// 注意:这里的&node.next 是一个*(*pointer_t)类型,需要转换为*unsafe.Pointer
return atomic.CompareAndSwapPointer(
(*unsafe.Pointer)(unsafe.Pointer(&node.next)),
unsafe.Pointer(oldPointer),
unsafe.Pointer(newPointer),
)
}
// 实际使用
func main() {
// 假设有一个初始节点和其next指针
initialNode := &node_t{value: "A"}
initialNext := &pointer_t{ptr: nil, count: 0}
initialNode.next = initialNext
// 尝试更新 initialNode 的 next 字段
// 假设我们要将 next 指向一个新的节点 B,并将计数器更新为 1
newNodeB := &node_t{value: "B"}
success := UpdateNextPointer(initialNode, initialNext, newNodeB, 1)
if success {
// 更新成功,initialNode.next 现在指向一个新的 pointer_t 实例
// 包含 newNodeB 和 count=1
println("Update successful!")
} else {
println("Update failed, another goroutine might have modified it.")
}
}在实际的无锁数据结构实现中,这两种技术各有优劣。位窃取适用于需要极高性能且额外信息量极小(如布尔标记或小计数器)的场景,但其实现复杂且有平台依赖性。写时复制(COW)则更为通用和安全,适用于各种复杂结构体,但会引入额外的内存分配开销。
在Go语言的并发编程实践中,可以参考一些开源项目来理解这些模式的应用。例如,tux21b/goco 中的无锁链表实现,大量使用了atomic.CompareAndSwapPointer,并引入了一个MarkAndRef结构体。这个MarkAndRef结构体与本教程中的pointer_t非常相似,它通过一个布尔标记(mark)和一个指针(ref)来表示节点是否被逻辑删除,并使用COW模式进行原子更新。这为实现复杂无锁数据结构提供了宝贵的参考。
选择哪种策略取决于具体的应用场景、性能要求以及对代码复杂性的接受程度。理解这些底层机制对于构建高效、健壮的并发数据结构至关重要。
以上就是Go语言中结构体原子比较与交换(CAS)的实现策略的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号