探索Go语言的原子操作秘籍:sync/atomic.Value全解析
引言
在并发编程的世界里,数据的一致性和线程安全是永恒的话题。Go语言以其独特的并发模型——goroutine和channel,简化了并发编程的复杂性。然而,在某些场景下,我们仍然需要一种机制来保证操作的原子性。这就是sync/atomic.Value
发挥作用的地方。
原子性:并发编程的基石
原子性(atomicity) 是指一个或多个操作在执行过程中不会被中断的特性。这些操作要么全部完成,要么全部不执行,从而避免了中间状态的暴露。在Go中,sync/atomic
包提供了一组原子操作,而Value
类型则是一种特殊的原子操作,用于存储和读取单个值。
适用场景:读多写少的优化
sync/atomic.Value
利用了写时复制(Copy-On-Write,COW)技术,这使得它在读多写少的场景下表现卓越。由于COW的特性,频繁的读操作不需要加锁,而写操作则会产生一个新的副本,这在内存使用上可能不是最经济的,尤其是在内存较大且写操作频繁的情况下。
官方案例:配置信息的动态更新
让我们通过一个官方示例来了解Value
的使用。这个示例展示了如何使用Value
来动态更新和读取服务器配置:
package mainimport ("sync/atomic""time"
)func loadConfig() map[string]string {return make(map[string]string)
}func requests() chan int {return make(chan int)
}func main() {var config atomic.Value // holds current server configuration// Create initial config value and store into config.config.Store(loadConfig())go func() {// Reload config every 10 seconds// and update config value with the new version.for {time.Sleep(10 * time.Second)config.Store(loadConfig())}}()// Create worker goroutines that handle incoming requests// using the latest config value.for i := 0; i < 10; i++ {go func() {for r := range requests() {c := config.Load()// Handle request r using config c._, _ = r, c}}()}
}
原理解析:Value的内部机制
Value
的内部实现基于Go的interface{}
类型,通过unsafe
包来实现原子操作。下面是Value
的定义和写入操作的核心逻辑:
// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {v any
}
Value 的底层是一个 intreface 结构体类型,包含一个 interface 类型 v
// efaceWords is interface{} internal representation.
type efaceWords struct {typ unsafe.Pointerdata unsafe.Pointer
}
efaceWords 是 interface 类型的内部实现,包含类型和值
写入操作
写入操作的关键在于确保类型一致性和原子性。首次写入时,会禁用抢占,确保写入过程不会被中断。后续写入则会检查类型一致性,并原子性地更新数据。
var firstStoreInProgress byte// Store sets the value of the Value v to val.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).
func (v *Value) Store(val any) {if val == nil {panic("sync/atomic: store of nil value into Value")}vp := (*efaceWords)(unsafe.Pointer(v))vlp := (*efaceWords)(unsafe.Pointer(&val))for {typ := LoadPointer(&vp.typ)if typ == nil {// Attempt to start first store.// Disable preemption so that other goroutines can use// active spin wait to wait for completion.runtime_procPin()if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {runtime_procUnpin()continue}// Complete first store.StorePointer(&vp.data, vlp.data)StorePointer(&vp.typ, vlp.typ)runtime_procUnpin()return}if typ == unsafe.Pointer(&firstStoreInProgress) {// First store in progress. Wait.// Since we disable preemption around the first store,// we can wait with active spinning.continue}// First store completed. Check type and overwrite data.if typ != vlp.typ {panic("sync/atomic: store of inconsistently typed value into Value")}StorePointer(&vp.data, vlp.data)return}
}
流程:
-
如果传入的值为空会产生一个 panic
-
通过 unsafe.Pointer 将old value 和 new value 转化成 efaceWords 类型
-
进入 for 循环
-
如果 typ == nil 说明是第一次写入值,那么进入到第一次赋值的流程
- runtime_procPin() 禁止抢占,标记当前G在M上不会被抢占
- 使用 CompareAndSwapPointer 先尝试将
typ
设置为^uintptr(0)
这个中间状态。如果失败,则证明已经有别的线程抢先完成了赋值操作,那它就解除抢占锁,然后重新回到 for 循环第一步。 - 如果设置成功则进入赋值阶段,注意这里是 先赋值 data,再赋值 typ,因为我们是根据 typ 是否等于 nil 判断 对象是否被初始化,所以最后赋值 typ 才能确保对象完成了初始化。
-
如果 typ 不等于 nil
-
typ == unsafe.Pointer(&firstStoreInProgress) 判断初始化是否完成,未完成则回到 for 循环起始处
-
如果初始化对象完成,判断 typ != vlp.typ ,如果新写入的值不等于旧值则panic
-
StorePointer(&vp.data, vlp.data) 把 old value 原子性替换成 new value
-
// StorePointer atomically stores val into *addr. // Consider using the more ergonomic and less error-prone [Pointer.Store] instead. func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
-
读取操作
读取操作相对简单,它会原子性地获取当前值。如果值尚未初始化,将返回nil
。
// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (val any) {vp := (*efaceWords)(unsafe.Pointer(v))typ := LoadPointer(&vp.typ)if typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) {// First store not yet completed.return nil}data := LoadPointer(&vp.data)vlp := (*efaceWords)(unsafe.Pointer(&val))vlp.typ = typvlp.data = datareturn
}
流程:
-
首先载入 value
-
if typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) 判断写入过程是否初始化完成
-
data := LoadPointer(&vp.data) 原子性载入 old value
// LoadPointer atomically loads *addr. // Consider using the more ergonomic and less error-prone [Pointer.Load] instead. func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
- 定义一个新的值 vlp := (*efaceWords)(unsafe.Pointer(&val))
- 然后将 old value 赋值给 new value (COW 思想)
- 返回新的 value
1. 所以每次调用 Load 我们都是获取到了一个副本,所以可以保证在并发读写时候的线程安全
总结与最佳实践
sync/atomic.Value
是一个强大的工具,适用于需要高并发读取的场景。然而,它也有其局限性,特别是在内存使用和写入操作的频率上。在使用Value
时,应当考虑以下几点:
- 读多写少:
Value
最适合的场景是读操作远多于写操作。 - 内存效率:频繁的写入可能会因为COW机制导致内存使用增加。
- 类型安全:写入操作要求类型一致性,否则会引发panic
参考文献
- Go sync/atomic包文档
相关文章:
探索Go语言的原子操作秘籍:sync/atomic.Value全解析
引言 在并发编程的世界里,数据的一致性和线程安全是永恒的话题。Go语言以其独特的并发模型——goroutine和channel,简化了并发编程的复杂性。然而,在某些场景下,我们仍然需要一种机制来保证操作的原子性。这就是sync/atomic.V…...
【java深入学习第3章】利用 Spring Boot 和 Screw 快速生成数据库设计文档
免费多模型AI网站,支持豆包、GPT-4o、谷歌Gemini等AI模型,无限制使用,快去白嫖👉海鲸AI🔥🔥🔥 在开发过程中,数据库设计文档是非常重要的,它可以帮助开发者理解数据库结构࿰…...

继“三级淋巴结”之后,再看看“单细胞”如何与AI结合【医学AI|顶刊速递|05-25】
小罗碎碎念 24-05-25文献速递 今天想和大家分享的是肿瘤治疗领域的另一个热点——单细胞技术,我们一起来看看,最新出炉的顶刊,是如何把AI与单细胞结合起来的。 另外,今天是周末,所以会有两篇文章——一篇文献速递&…...

[图解]产品经理创新之阿布思考法
0 00:00:00,000 --> 00:00:01,900 那刚才我们讲到了 1 00:00:02,730 --> 00:00:03,746 业务序列图 2 00:00:03,746 --> 00:00:04,560 然后怎么 3 00:00:05,530 --> 00:00:06,963 画现状,怎么改进 4 00:00:06,963 --> 00:00:09,012 然后改进的模式…...

Proteus仿真小技巧(隔空连线)
用了好几天Proteus了.总结一下使用的小技巧. 目录 一.隔空连线 1.打开添加网络标号 2.输入网络标号 二.常用元件 三.运行仿真 四.总结 一.隔空连线 引出一条线,并在末尾点一下. 1.打开添加网络标号 选择添加网络标号, 也可以先点击按钮,再去选择线(注意不要点端口) 2.…...

抖音极速版:抖音轻量精简版本,新人享大福利
和快手一样,抖音也有自己的极速版,可视作抖音的轻量精简版,更专注于刷视频看广告赚钱,收益比抖音要高,可玩性更佳。 抖音极速版简介 抖音极速版是一个提供短视频创业和收益任务的平台,用户可以通过观看广…...

leetCode-hot100-数组专题之双指针
数组双指针专题 1.同向双指针1.1例题26.删除有序数组中的重复项27.移除元素80.删除有序数组中的重复项 Ⅱ 2.相向双指针2.1例题11.盛最多水的容器42.接雨水581.最短无序连续子数组 双指针在算法题中很常见,下面总结双指针在数组中的一些应用,主要分为两类…...

完成商品SPU管理页面
文章目录 1.引入前端界面1.将前端界面放到commodity下2.创建菜单3.进入前端项目,使用npm添加依赖1.根目录下输入2.报错 chromedriver2.27.2的问题3.点击链接下载压缩包,然后使用下面的命令安装4.再次安装 pubsub-js 成功5.在main.js中引入这个组件 4.修改…...

Ansible实战YAML语言完成apache的部署,配置,启动全过程
🏡作者主页:点击! 🏝️Ansible专栏:点击! ⏰️创作时间:2024年5月24日15点59分 目录 💯趣站推荐💯 🎊前言 ✨️YAML语言回顾 🎆1.编写YAML文…...
深入探索微软Edge:新一代浏览器的演进与创新
在数字时代的浪潮中,浏览器已不再只是简单的网页访问工具,而是成为了连接信息、服务与用户之间的重要桥梁。微软Edge作为微软公司推出的一款全新的浏览器,不仅承载着微软在互联网领域的最新愿景,还融合了多项前沿技术,…...
k8s使用Volcano调度gpu
k8s部署 https://www.yangxingzhen.com/9817.html cri-dockerd安装 https://zhuanlan.zhihu.com/p/632861515 安装nvidia-container-runtime https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html 安装k8s-device-plugin https://…...
x的平方根-力扣
本题想到使用二分法不断逼近一个区间,直到最后趋近于x,从而求得解。注意的点,一开始使用 if(mid * mid < x) 进行判断时,会出现越界,原因是输入一个很大的数是,超过int表示的范围,继而修改为…...

hot100 -- 回溯(上)
目录 🍞科普 🌼全排列 AC DFS 🚩子集 AC DFS 🎂电话号码的字母组合 AC DFS 🌼组合总和 AC DFS 🍞科普 忘记 dfs 的,先看看这个👇 DFS(深度优先搜索…...
5.24数据库作业
考虑如下关系模式R(A,B.C.D,E,F)上的函数依赖集F: {A→BCD,BC→DE,B→D,D→A} 1、计算B的闭包。 2、(使用Armstrong公理)证明AF是超码。 3、计算上述函数依赖集F的正则覆盖;给出你的推导的步骤并解释。 4、基于正则覆盖࿰…...

go-zero 实战(5)
引入Prometheus 用 Prometheus 监控应用 1. 用 docker 启动 Prometheus 编辑配置位置,我将 prometheus.yaml 和 targets.json 文件放在了 /opt/prometheus/conf目录下 prometheus.yaml global:scrape_interval: 15s # 抓取间隔evaluation_interval: 15s # 评估…...

Python异常处理:打造你的代码防弹衣!
Hi,我是阿佑,上文咱们讲到——揭秘Python的魔法:装饰器的超能力大揭秘 ♂️✨,阿佑将带领大家通过精准捕获异常、使用with语句和上下文管理器、以及异常链等高级技巧来增强代码的健壮性。就像为代码穿上防弹衣,保护它…...

Linux——进程与线程
进程与线程 前言一、Linux线程概念线程的优点线程的缺点线程异常线程用途 二、Linux进程VS线程进程和线程 三、Linux线程控制创建线程线程ID及进程地址空间布局线程终止线程等待分离线程 四、习题巩固请简述什么是LWP请简述LWP与pthread_create创建的线程之间的关系简述轻量级进…...

ping 探测网段哪些地址被用
#!/bin/bash# 遍历192.168.3.1到192.168.3.254 for i in {1..254} doip"192.168.3.$i"# 对每个IP地址进行三次ping操作if ping -c 3 -W 1 $ip > /dev/null 2>&1thenecho "$ip: yes"fi done$ sh test.sh 192.168.3.1: yes 192.168.3.95: yes 192.…...

OSPF问题
.ospf 选路 域内 --- 1类,2类LSA 域间 --- 3类LSA 域外 --- 5类,7类LSA --- 根据开销值的计算规则不同,还分为类型1和类型2 ospf 防环机制 区域内防环:在同一OSPF区域内,所有路由器通过交换链路状态通告ÿ…...
asgasgas
asdgasdgsa...

测试W5500的第11步_使用ARP解析IP地址对应的MAC地址
本文介绍了基于W5500芯片的ARP协议实现方法,详细阐述了ARP请求与回复的工作机制。ARP协议通过广播请求和单播回复实现IP地址与MAC地址的映射,确保局域网设备间的可靠通信。文章提供了完整的STM32F10x开发环境下的代码实现,包括网络初始化、SP…...

【办公类-104-01】20250606通义万相50分一天用完,通义万相2.1专业版测试
背景需求: 昨天打开通义万相,发现分数降低到3位数,原来时1500.仔细看,原来每天的50分,只有1天有效期了。 用掉试试,用的是之前的30天积分,还是今天的1天积分 纯白色背景,卡通简笔画…...
机器学习笔记【Week7】
一、SVM的动机:大间隔分类器 1、逻辑回归回顾 假设函数为 sigmoid 函数: h θ ( x ) 1 1 e − θ T x h_\theta(x) \frac{1}{1 e^{-\theta^Tx}} hθ(x)1e−θTx1 分类依据是 h θ ( x ) ≥ 0.5 h_\theta(x) \geq 0.5 hθ(x)≥0.5 为正类&a…...
CentOS 7 如何安装llvm-project-10.0.0?
CentOS 7 如何安装llvm-project-10.0.0? 需要先升级gcc至7.5版本,详见CentOS 7如何编译安装升级gcc版本?一文 # 备份之前的yum .repo文件至 /tmp/repo_bak 目录 mkdir -p /tmp/repo_bak && cd /etc/yum.repo.d && /bin/mv ./*.repo …...
CVAT标注服务
CVAT 是一个交互式的视频和图像标注工具,适用于计算机视觉,是一个典型的现代Web应用架构,可以实现大部分情况的标注工作,可以通过serveless CVAT-github cvat文档 下面将就其配置介绍一下几个服务: 1. 核心服务 (C…...
Android四大组件通讯指南:Kotlin版组件茶话会
某日,Android王国举办Kotlin主题派对。Activity穿着Jetpack Compose定制礼服,Service戴着协程手表,BroadcastReceiver拿着Flow喇叭,ContentProvider抱着Room数据库入场。它们正愁如何交流,Intent举着"邮差"牌…...

光电耦合器:数字时代的隐形守护者
在数字化、自动化高速发展的今天,光电耦合器正以一种低调却不可或缺的方式,悄然改变着我们的生活。它不仅是电子电路中的“安全卫士”,更是连接信号世界的“桥梁”,凭借出色的电气隔离能力,为各类设备提供稳定可靠的信…...
11. vue pinia 和react redux、jotai对比
对比 Vue 的 Pinia,和 React 的 Redux、Jotai,分中英文简要介绍、特性、底层原理、使用场景。 简单介绍 1.1 Pinia(Vue) • 英文:Pinia is the official state management library for Vue 3, designed to be simple…...

《汇编语言》第13章 int指令
中断信息可以来自 CPU 的内部和外部,当 CPU 的内部有需要处理的事情发生的时候,将产生需要马上处理的中断信息,引发中断过程。在第12章中,我们讲解了中断过程和两种内中断的处理。 这一章中,我们讲解另一种重要的内中断…...
UI学习—cell的复用和自定义cell
前言 Nib是什么? Nib就是.xib文件:一个可视化的UI界面文件,它记录了一个UI组件(例如一个表格单元格Cell)的界面布局信息,可以在interfaceBuilder中创建 [UINib nibWithNibName:"CustomCell" b…...