
在构建高性能的并发数据结构,特别是无锁(lock-free)队列时,atomic.compareandswap操作是核心。然而,当需要对包含多个字段的结构体(例如,一个指针和一个计数器组合的pointer_t)执行原子cas时,go语言的sync/atomic包并不能直接支持。这是因为底层硬件架构通常只提供对单个机器字(如int32, int64, 或 unsafe.pointer)的原子操作。像伪代码中cas(&tail.ptr->next, next, <node, next.count+1>)这样,同时更新一个指针和一个计数器的操作,在go中无法直接通过sync/atomic实现。
为了克服这一限制,开发者需要采用一些巧妙的策略来模拟或实现对复杂结构体的原子更新。以下介绍两种常用的方法。
原理: 在64位系统中,内存地址通常是8字节对齐的。这意味着指针的低位(例如,最低3位)总是0,因为地址必须是8的倍数。我们可以利用这些未使用的低位来存储额外的小型数据,例如一个计数器或一个布尔标记。通过将这些信息编码到指针中,我们可以将一个“结构体”的原子操作转化为一个“指针”的原子操作,从而利用atomic.CompareAndSwapPointer。
适用场景: 当需要嵌入的信息(如计数器或布尔标记)足够小,可以放入指针的未使用位时。
实现细节:
示例代码(概念性):
假设node_t是8字节对齐的,我们可以使用uintptr的低3位来存储一个uint计数器(最大值7)。
立即学习“go语言免费学习笔记(深入)”;
package main
import (
"fmt"
"sync/atomic"
"unsafe"
)
// node_t 模拟链表节点
type node_t struct {
value interface{}
// 其他字段
}
// pointer_t 包含一个节点指针和一个计数器
// 在位窃取策略中,我们不会直接使用这个结构体,而是将其信息编码到 uintptr 中
// type pointer_t struct {
// ptr *node_t
// count uint
// }
// 掩码定义:假设低3位用于计数器,其余位用于指针
const (
countMask = 0x7 // 000...0111,用于获取计数器
ptrMask = ^countMask // 111...1000,用于获取指针
)
// encode 将 *node_t 和 uint 编码成一个 uintptr
func encode(ptr *node_t, count uint) uintptr {
// 确保计数器不会溢出可用位数
if count > countMask {
panic("count exceeds available bits")
}
// 将指针转换为 uintptr,并清除其低位(因为是8字节对齐,低3位通常为0)
// 然后将计数器编码到这些低位
return (uintptr(unsafe.Pointer(ptr)) & ptrMask) | (uintptr(count) & countMask)
}
// decode 从编码后的 uintptr 中解码出 *node_t 和 uint
func decode(encoded uintptr) (*node_t, uint) {
ptr := (*node_t)(unsafe.Pointer(encoded & ptrMask))
count := uint(encoded & countMask)
return ptr, count
}
func main() {
// 模拟一个需要原子更新的 "next" 字段
var atomicNext uintptr // 使用 uintptr 来存储编码后的指针和计数器
// 初始状态
initialNode := &node_t{value: "A"}
initialCount := uint(0)
initialEncoded := encode(initialNode, initialCount)
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&atomicNext)), unsafe.Pointer(initialEncoded))
fmt.Printf("初始值: ptr=%p, count=%d, encoded=0x%x\n", initialNode, initialCount, initialEncoded)
// 尝试进行 CAS 操作
// 假设我们想将 next 更新为 newNodeB 和 count+1
newNodeB := &node_t{value: "B"}
expectedEncoded := initialEncoded // 期望的旧值
newEncoded := encode(newNodeB, initialCount+1) // 编码新值
// 执行 CAS
// 注意:CompareAndSwapPointer 期望 *unsafe.Pointer, old, new
// 我们需要将 uintptr 转换为 unsafe.Pointer
swapped := atomic.CompareAndSwapPointer(
(*unsafe.Pointer)(unsafe.Pointer(&atomicNext)),
unsafe.Pointer(expectedEncoded),
unsafe.Pointer(newEncoded),
)
if swapped {
fmt.Println("CAS 成功!")
} else {
fmt.Println("CAS 失败!")
}
// 读取更新后的值
currentEncoded := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&atomicNext)))
currentPtr, currentCount := decode(uintptr(currentEncoded))
fmt.Printf("更新后值: ptr=%p, count=%d, encoded=0x%x\n", currentPtr, currentCount, currentEncoded)
fmt.Printf("更新后节点值: %v\n", currentPtr.value)
}注意事项:
原理: 写时复制是一种更通用的策略,适用于任何大小和复杂度的结构体。其核心思想是,不是直接修改结构体的字段,而是将结构体本身视为不可变的。当需要“修改”结构体时,我们创建一个该结构体的完整副本,对副本进行修改,然后原子地将一个指向旧结构体的指针替换为指向新结构体的指针。
适用场景: 当结构体较大、包含复杂字段,或嵌入信息无法通过位窃取实现时。这是更推荐和更通用的方法。
实现细节:
示例代码:
package main
import (
"fmt"
"sync/atomic"
"unsafe"
)
type pointer_t struct {
ptr *node_t
count uint
}
type node_t struct {
value interface{}
next *pointer_t // 关键改变:next 现在是一个指向 pointer_t 的指针
}
func main() {
// 初始状态
initialNode := &node_t{value: "A"}
initialPointerT := &pointer_t{ptr: initialNode, count: 0}
// 假设这是一个全局或共享的节点,其 next 字段需要原子更新
var headNode node_t
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&headNode.next)), unsafe.Pointer(initialPointerT))
fmt.Printf("初始值: headNode.next 指向 %p, 包含 ptr=%p, count=%d\n", initialPointerT, initialPointerT.ptr, initialPointerT.count)
// 尝试进行 CAS 操作
// 假设我们想将 headNode.next 更新为指向 newNodeB 和 count+1
newNodeB := &node_t{value: "B"}
// 循环直到 CAS 成功
for {
// 1. 获取当前 headNode.next 指针
oldNextPtrValue := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&headNode.next)))
oldPointerT := (*pointer_t)(oldNextPtrValue) // 解引用得到当前的 pointer_t 结构体
// 2. 创建新的 pointer_t 实例(副本)并进行修改
// 注意:这里我们创建一个新的结构体,而不是修改 oldPointerT
newPointerT := &pointer_t{
ptr: newNodeB,
count: oldPointerT.count + 1,
}
// 3. 尝试原子交换:将旧指针替换为新指针
swapped := atomic.CompareAndSwapPointer(
(*unsafe.Pointer)(unsafe.Pointer(&headNode.next)), // 目标地址
oldNextPtrValue, // 期望的旧值(指针)
unsafe.Pointer(newPointerT), // 新值(指针)
)
if swapped {
fmt.Println("CAS 成功!")
break // 成功,退出循环
}
// 如果 CAS 失败,说明 headNode.next 已被其他协程修改,需要重试
fmt.Println("CAS 失败,重试...")
}
// 读取更新后的值
currentNextPtrValue := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&headNode.next)))
currentPointerT := (*pointer_t)(currentNextPtrValue)
fmt.Printf("更新后值: headNode.next 指向 %p, 包含 ptr=%p, count=%d\n", currentPointerT, currentPointerT.ptr, currentPointerT.count)
fmt.Printf("更新后节点值: %v\n", currentPointerT.ptr.value)
}注意事项:
在实际的无锁数据结构实现中,这些技术被广泛应用。例如,在tux21b/goco项目中,作者实现了一个锁无关列表。其中定义了一个名为MarkAndRef的结构体,它类似于pointer_t,用于存储一个布尔标记和一个指针。该实现通过atomic.CompareAndSwapPointer对MarkAndRef的实例进行原子操作,以确保在插入元素时节点未被标记为删除。这个案例展示了如何将复杂状态(指针+标记)编码成一个可由atomic.CompareAndSwapPointer操作的单元,为构建无锁数据结构提供了宝贵的参考。
在Go语言中,由于底层硬件的限制,我们无法直接对包含多个字段的结构体执行原子比较与交换操作。为了实现对复杂结构体的原子更新,我们可以采用两种主要策略:
选择哪种策略取决于具体的应用场景和需求。对于简单的状态(如指针加一个布尔标记),位窃取可能更高效。而对于更复杂或可能需要频繁修改的结构体,写时复制则是一个更健壮和可扩展的解决方案。理解并掌握这些技术对于在Go中构建高性能、并发安全的无锁数据结构至关重要。
以上就是Go语言中结构体原子比较与交换:实现无锁数据结构的策略的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号