惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Simon Willison's Weblog
Simon Willison's Weblog
T
Troy Hunt's Blog
L
Lohrmann on Cybersecurity
S
Schneier on Security
Spread Privacy
Spread Privacy
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
G
GRAHAM CLULEY
博客园 - 【当耐特】
有赞技术团队
有赞技术团队
SecWiki News
SecWiki News
博客园 - 叶小钗
博客园 - Franky
V
Vulnerabilities – Threatpost
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
O
OpenAI News
小众软件
小众软件
V
V2EX
N
News and Events Feed by Topic
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
The Hacker News
The Hacker News
Project Zero
Project Zero
The Last Watchdog
The Last Watchdog
雷峰网
雷峰网
Google Online Security Blog
Google Online Security Blog
T
Tailwind CSS Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
量子位
D
Docker
Recent Announcements
Recent Announcements
T
Threat Research - Cisco Blogs
P
Privacy International News Feed
爱范儿
爱范儿
PCI Perspectives
PCI Perspectives
Jina AI
Jina AI
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
大猫的无限游戏
大猫的无限游戏
V2EX - 技术
V2EX - 技术
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The Register - Security
The Register - Security
T
The Blog of Author Tim Ferriss
博客园 - 聂微东
Cloudbric
Cloudbric
S
Security Affairs
F
Fortinet All Blogs

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
sync/atomic Code Reading
2023-04-21 · via 蛮荆

2023-04-21 Golang 并发编程 Go 源码分析 读代码

概述

sync/atomic 包提供了原子同步操作原语,整个操作过程无需加锁,也不会产生 goroutine 上下文切换。

API

atomic API

Swap 操作由 SwapT 系列函数 (例如 SwapInt32, SwapInt64) 实现,等价于如下的原子操作:

old = *addr
*addr = new
return old

CAS 操作由 CompareAndSwapT 系列函数 (例如 CompareAndSwapInt32, CompareAndSwapInt64) 实现,等价于如下的原子操作:

if *addr == old {
    *addr = new
    return true
}
return false

Add 操作由 AddT 系列函数 (例如 AddInt32, AddInt64) 实现,等价于如下的原子操作:

*addr += delta
return *addr

Load And Store 操作由 LoadT and StoreT 系列函数 (例如 LoadInt32, LoadInt64) 实现,等价于如下的原子操作:

内部实现

我们来探究一下 sync/atomic 包的内部实现,文件目录路径为 $GOROOT/src/sync/atomic,笔者的 Go 版本为 go1.19 linux/amd64。 需要注意的是,该文件内只给出了函数的定义,函数的实现为在对应的 asm.s 汇编文件中。

函数声明

Swap, CAS 等操作的函数原型全部定义在 doc.go 文件中。

// 在 386 上, 64 位函数使用的指令在 Pentium MMX 之前不可用
// 在非 Linux ARM 上,64 位函数使用的指令在 ARMv6k core 之前不可使用
// 在 ARM 上,386 和 32 位 MIPS,调用方负责以原子的方式访问 64 位对齐的 64 位字
// 全局变量、局部变量、已经分配的结构体、数组、切片中的第一个字可以依赖于 64 位对齐

func SwapInt32(addr *int32, new int32) (old int32)

func SwapInt64(addr *int64, new int64) (old int64)

...


func StoreUintptr(addr *uintptr, val uintptr)

func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

函数实现

Swap, CAS 等操作的函数实现全部在 asm.s 汇编文件中,但是该文件中的函数并不是直接实现,而是 “套了一层壳”, 最终的实现在平台体系结构对应的汇编文件中,目录为$GOROOT/src/runtime/internal/atomic。 例如: 笔者的 Go 版本为 go1.19 linux/amd64, 那么就会跳转到 runtime/internal/atomic/atomic_amd64.s 文件对应的函数。

不同平台体系结构的实现

这里以 ·SwapInt32 汇编函数为例,来看下函数的跳转和实现。

# asm.s 文件

TEXT ·SwapInt32(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xchg(SB)

函数的实现中使用了 JMP 跳转指令,最终跳转到了文件 $GOROOT/src/runtime/internal/atomic/atomic_amd64.s 中的 ·Xchg 函数。

// atomic_amd64.s 文件

TEXT ·Xchg(SB), NOSPLIT, $0-20
	MOVQ	ptr+0(FP), BX   // 参数 1,8 字节 *int32 指针
	MOVL	new+8(FP), AX   // 参数 2,4 字节 int32
	XCHGL	AX, 0(BX)       // 交换指令
	MOVL	AX, ret+16(FP)  // 交换后的 AX(old value) 写入 FP 伪寄存器返回值位
	RET

// 上面的汇编代码等价于如下 Go 代码
// uint32 Xchg(ptr *uint32, new uint32)
// Atomically:
//	old := *ptr
//	*ptr = new
//	return old

atomic.Value

接下来看一下原子数据类型 atomic.Value 的内部实现。

Value 对象

Value 数据类型提供了一致性原子性的 Swap, CAS 等方法,其中

  • Load 方法调用时,Value 的零值返回 nil
  • Store 方法一旦被调用,Value 对象就不能再复制
  • Value 对象一旦使用后,就不能再复制
type Value struct {
    v any
}

ifaceWords 对象

ifaceWords 对象是 Value 对象对应的 数据类型 + 值 的内部抽象表示,虽然 Value 对象相关方法的参数类型是 any,但是内部操作的都是 ifaceWords 对象 (通过类型转换机制)。

type ifaceWords struct {
	typ  unsafe.Pointer // 类型
	data unsafe.Pointer // 值
}

ifaceWords 对象

Value.Load 方法

Load 返回最新设置的值,如果未对该值调用过 Store 方法 (Value 处于零值状态),返回 nil。

func (v *Value) Load() (val any) {
	// 类型转化
	vp := (*ifaceWords)(unsafe.Pointer(v))
	// 原子获取值类型
	typ := LoadPointer(&vp.typ)
    // 从未调用过 Store 方法
	if typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) {
		return nil
	}
	
	// 原子获取值
	data := LoadPointer(&vp.data)
	// 返回值类型转换
	vlp := (*ifaceWords)(unsafe.Pointer(&val))
	// 返回值类型
	vlp.typ = typ
	// 返回值数据值
	vlp.data = data
	return
}

Value.Store 方法

Store 方法设置 Value 对象的值,对于某个特定的 Value 对象,每次调用 Store 方法时都必须使用相同的数据类型值 (例如第一次存储的是 int 类型, 那么之后调用时必须传递 int 类型), 类型不一致时会产生 panic, 传递 nil 参数 ( Store(nil) ) 同样会产生 panic

func (v *Value) Store(val any) {
	if val == nil {
		// 参数为 nil, 直接 panic
		panic("sync/atomic: store of nil value into Value")
	}
	
	// 参数类型转换
	vp := (*ifaceWords)(unsafe.Pointer(v))
	// 返回值类型转换
	vlp := (*ifaceWords)(unsafe.Pointer(&val))
	
	for {
		// 原子获取参数值数据类型
		typ := LoadPointer(&vp.typ)
		
		if typ == nil {
			// 如果类型为 nil, 尝试开始第一次 Store
			// 禁止抢占(避免操作未完成时,被其他 goroutine 抢占),其他 goroutine 可以使用自旋锁来等待完成
			// 同时可以避免 GC 的时候看到 unsafe.Pointer(^uintptr(0)) 这个中间状态的值
			
			runtime_procPin() // 禁止抢占 (具体的实现这里先忽略)
			
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				// 如果设置值的类型时失败
				// 说明当前 goroutine 在禁止抢占执行结束前,已经有其他 goroutine 设置完了值类型
				runtime_procUnpin() // 恢复抢占
				continue
			}
			
			// 完成第一次 Store 操作
			StorePointer(&vp.data, vlp.data)
			StorePointer(&vp.typ, vlp.typ)
			
			runtime_procUnpin() // 恢复抢占 (具体的实现这里先忽略)
			return
		}
		
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			// 第一次 Store 操作正在执行中,等待...
			// [待优化项] 因为在第一次 Store 禁用了抢占,所以可以使用自旋锁等待完成
			continue
		}
		
		// 已经完成了第一次 Store 操作, 检查类型并覆盖数据(检查是否和第一次设置的数据类型一致)
		if typ != vlp.typ {
			// 当前数据类型和第一次设置的数据类型不一致,产生 panic
			panic("sync/atomic: store of inconsistently typed value into Value")
		}
		
		StorePointer(&vp.data, vlp.data)
		return
	}
}

Value.Swap 方法

Swap 方法将新值存储到 Value 对象,并返回 Value 对象的旧值,如果 Value 为空, 则返回 nil。 对于某个特定的 Value 对象,每次调用 Swap 方法时都必须使用相同的数据类型值 (例如第一次存储的是 int 类型, 那么之后调用时必须传递 int 类型), 类型不一致时会产生 panic, 传递 nil 参数 ( Swap(nil) ) 同样会产生 panic

func (v *Value) Swap(new any) (old any) {
	if new == nil {
        // 参数为 nil, 直接 panic
		panic("sync/atomic: swap of nil value into Value")
	}
	
    // 当前值类型转换
	vp := (*ifaceWords)(unsafe.Pointer(v))
	// 参数类型转换
	np := (*ifaceWords)(unsafe.Pointer(&new))
	
	for {
		// 原子获取当前值数据类型
		typ := LoadPointer(&vp.typ)
		
		if typ == nil {
            // Value 对象还没有被设置
            // 尝试开始第一次 Store 操作
			
			// 流程和 Store 方法内部类型,这里直接省略掉
			...

			return nil
		}
		
        ...
		
        // 已经完成了第一次 Store 操作, 检查类型并覆盖数据(检查是否和第一次设置的数据类型一致)
		if typ != np.typ {
			// 当前数据类型和第一次设置的数据类型不一致,产生 panic
			panic("sync/atomic: swap of inconsistently typed value into Value")
		}
		
		// 返回值类型转换
		op := (*ifaceWords)(unsafe.Pointer(&old))
		op.typ, op.data = np.typ, SwapPointer(&vp.data, np.data)
		return old
	}
}

Value.CompareAndSwap 方法

CompareAndSwap 执行 CAS 操作,对于某个特定的 Value 对象,每次调用 CompareAndSwap 方法时都必须使用相同的数据类型值 (例如第一次存储的是 int 类型, 那么之后调用时必须传递 int 类型), 类型不一致时会产生 panic, 传递 nil 参数 ( CompareAndSwap(old, nil) ) 同样会产生 panic

func (v *Value) CompareAndSwap(old, new any) (swapped bool) {
	if new == nil {
        // 参数为 nil, 直接 panic
		panic("sync/atomic: compare and swap of nil value into Value")
	}
	
	// 当前值类型转换
	vp := (*ifaceWords)(unsafe.Pointer(v))
	// 参数新值类型转换
	np := (*ifaceWords)(unsafe.Pointer(&new))
	// 参数旧值类型转换
	op := (*ifaceWords)(unsafe.Pointer(&old))
	
	if op.typ != nil && np.typ != op.typ {
		// 新值和旧值数据类型不一致
		panic("sync/atomic: compare and swap of inconsistently typed values")
	}
	
	for {
		// 原子获取当前值数据类型
		typ := LoadPointer(&vp.typ)
		if typ == nil {
			if old != nil {
				// 当前值和参数值类型不一样
				return false
			}

            // 流程和 Store 方法内部类型,这里直接省略掉
			...
			
			return true
		}
		
        ...
		
		if typ != np.typ {
			// 参数新值数据类型和当前值的数据类型不一致,产生 panic
			panic("sync/atomic: compare and swap of inconsistently typed value into Value")
		}
		
		// CompareAndSwapPointer 函数只能确保 vp.data 从获取到之后没有发生变化
		data := LoadPointer(&vp.data)
		
		// 拷贝当前值的变量,然后和参数旧值进行比较 
		var i any
		// 当前值类型
		(*ifaceWords)(unsafe.Pointer(&i)).typ = typ
		// 当前值数据
		(*ifaceWords)(unsafe.Pointer(&i)).data = data
		if i != old {
			// 当前值已经发生变化
			return false
		}
		
		// 调用 CAS 操作 (内部再调用对应的汇编)
		return CompareAndSwapPointer(&vp.data, data, np.data)
	}
}

小结

atomic 实现了同步算法的底层内存原子操作原语,其内部实现主要是汇编 (各个平台对应各自不同的指令,通过编译器链接),使用这些函数时需要更加谨慎, 官方给出的建议是除了特殊的、底层的应用程序外,其他情况最好使用 channel 或其他同步原语来完成 (但是从大多数开源组件实现代码来看,并没有遵守官方的建议)。

标准库还提供了 atomic.Value 原子数据类型,并且为该类型实现了常见的原子操作,如 Load, Store, CAS 等, 在实现类似 对象需要原子操作 这样的功能时可以直接复用该类型,例如标准库 context 包的内部实现中里面就用到了 atomic.Value