Go语言的原子操作

news2025/6/4 11:16:23

当我们想要对某个变量并发安全的修改,除了使用官方提供的mutex,还可以使用sync/atomic包的原子操作,它能够保证对变量的读取或修改期间不被其他的协程所影响。

Golang提供的原子操作都是非侵入式的,由标准库sync/atmoic包提供,直接由底层CPU硬件支持。

也就是在硬件层次去实现的,性能较好,不需要像mutex那样记录很多状态。当然,mutex不止是对变量的并发控制,更多的是对代码块的并发控制,两者侧重点不一样。

一.介绍

原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。

具体的原子操作在不同的操作系统中实现是不同的。比如在Intel的CPU架构机器上,主要是使用总线锁的方式实现的。大致的意思就是当一个CPU需要操作一个内存块的时候,向总线发送一个LOCK信号,所有CPU收到这个信号后就不对这个内存块进行操作了。等待操作的CPU执行完操作后,发送UNLOCK信号,才结束。在AMD的CPU架构机器上就是使用MESI一致性协议的方式来保证原子操作。所以我们在看atomic源码的时候,我们看到它针对不同的操作系统有不同汇编语言文件。

Golang在sync包中已经提供了锁,为什么还需要使用atomic原子操作呢?

1)加锁的代价比较高,耗时多,需要上下文切换。

2)原子操作只针对基本数据类型,不支持结构体、自定义数据类型

3)原子操作在用户态可以完成,性能比互斥锁要高。

4)针对特定需求原子操作步骤简单,无需加锁解锁步骤。

为什么 atomic 比mutex快?

1)原子操作很快,因为它们依赖于CPU指令而不是依赖外部锁。使用互斥锁时,每次获得锁时,goroutine都会短暂暂停或中断,这种阻塞占使用互斥锁所花费时间的很大一部分(他们是由操作系统调度的)。原子操作可以在没有任何中断的情况下执行。

2)原子操作是能够保证执行期间是连续且不会被中断的,临界区只能保证访问共享数据是按顺序访问的,但并不能保证访问期间不会被切换上下文。

CAS

CAS是CPU硬件同步原语,是Compare And Swap的缩写

Go中的CAS操作,是借用了CPU提供的原子性指令来实现。CAS操作修改共享变量时候不需要对共享变量加锁,

而是通过类似乐观锁的方式进行检查,本质还是不断的占用CPU资源换取加锁带来的开销(比如上下文切换开销)

原子操作中的CAS,在sync/atomic包中,这类原子操作由名称以CompareAndSwap为前缀的若干个函数提供

优势:

  • 可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性育

劣势:

  • 在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。因为需要对oLd值进行匹配,只有匹配成功

当前atomic 包有以下几种原子操作:Add、CompareAndSwap、Load、Store、Swap

二.操作

Add,CompareAndSwap,Load,Store,Swap

2.1 Add (增或减)

  1. 用于处理增加和减少的原子操作,函数名以Add为前缀,后跟针对特定类型的名称。
  2. 原子增被操作的只能是数值类型,即int32,int64,uint32,uint64,uinptr。
  3. 原子增减函数的第一个参数为原值,第二个是要增多少。
  4. 方法:
func AddInt32(addr *int32, delta int32) (new int32)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var num int32
	fmt.Println("num is ", num)
	atomic.AddInt32(&num, 10)
	fmt.Println("new num is ", num)
}

减法就很简单,就直接负值即可,就可以实现。

2.2 CompareAndSwap (比较并交换)

什么是比较并交换?

我们来看它的参数,有一个old和new值,分别表示它的原始值和新值,如果说这个原始值输入不对,则不会改变这个new值。

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var num int32
	fmt.Println("num is ", num)
	atomic.CompareAndSwapInt32(&num, 0, 10)
	fmt.Println("new num is ", num)
}

可以试试,把old值不设置为0,它并不报错,只是不改变值而已

2.3 Load (读取)

Load原子性的读取操作接受一个对应类型的指针值,返回该指针指向的值。原子性读取意味着读取值的同时,当前计算机的任何CPU都不会进行针对值的读写操作。

比方说在32位计算架构的计算机上写入一个64位的整数时,如果在这个写操作尚未完成的时候,有一个读操作被并发的执行了,那么这个读操作很有可能会读取到一个只有被修改了一半的数据。

另一个需要注意的是for循环中,当使用v:=value为变量v赋值时,需要注意的是由于读取value的值的操作并不是并发安全的。因此在读取操作时其它对其的读写操作可能会同时发生。

先来看下它的函数和简单使用

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var num int32
	fmt.Println("num is ", num)
	atomic.LoadInt32(&num)
	fmt.Println("new num is ", atomic.LoadInt32(&num))
}

针对上述所说的问题做一个详细的解答

首先就是非原子性读取的风险:

假设有一个共享变量 value,多个协程(goroutine)可能同时读写它。如果直接通过普通读取(如 v := value)获取值,可能会遇到以下问题:

  • 部分写入:例如在 32 位系统上写入一个 64 位整数需要两次操作(先写低 32 位,再写高 32 位)。如果读取操作在两次写入之间发生,可能读到“半新半旧”的值。
  • 缓存不一致:不同 CPU 核心的缓存可能不同步,普通读取可能读到过时的缓存值,而非内存中的最新值。

这个时候就该我们的Load出手了:

  1. 它可以确保读操作是从内存中获取,而不是缓存
  2. 原子性保证,确保读到的值是完整的,不会读到部分写入

2.4 Store(存储)

  1. 原子性存储会将val值保存在*addr中
  2. 与读操作对应的写操作,sync/atomic提供了与原子值载入Load函数相对应的原子值存储Store函数,原子性存储函数均为Store开头
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var num int32
	atomic.StoreInt32(&num, 10)
	fmt.Println("num is ", num)
}

2.5 Swap (交换)

他和比较并交换是不同的,他只有新值作为参数,但是旧值是返回的

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var num int32
	old := atomic.SwapInt32(&num, 10)
	fmt.Println("new is ", num)
	fmt.Println("old is", old)
}

2.6 Or (位或)和 And (位与)

都是用于设置某些特定位(如标志位),不影响其他位。

就是用于处理bit位,专门用于做位运算的。

func OrInt32(addr *int32, mask int32) (old int32)
func OrUint32(addr *uint32, mask uint32) (old uint32)
func OrInt64(addr *int64, mask int64) (old int64)
func OrUint64(addr *uint64, mask uint64) (old uint64)
func OrUintptr(addr *uintptr, mask uintptr) (old uintptr)

func AndInt32(addr *int32, mask int32) (old int32)
func AndUint32(addr *uint32, mask uint32) (old uint32)
func AndInt64(addr *int64, mask int64) (old int64)
func AndUint64(addr *uint64, mask uint64) (old uint64)
func AndUintptr(addr *uintptr, mask uintptr) (old uintptr)

三.atomic.Value

网上对这个有详细的介绍底层,感兴趣的同学可以自行上网查阅

上述的操作你会发现,他只针对了一些一部分类型,其他数据结构依旧要使用到锁,如果把这些互斥锁换成用atomic.LoadPointer/StorePointer来做并发控制,那性能将能提升。

针对这个问题,就有人提出,在已有的atomic包的基础之上,封装出一个atomic.Value类型,这样用户就可以在不依赖Go内部类型的情况下使用原子操作了。

在他下面也提供了上述的Add,Swap,CompareAndSwap,Load和Store

type Value struct {
	v any
}
package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

// 定义配置结构体
type Config struct {
	DatabaseURL string
	MaxConnects int
	Timeout     time.Duration
}

func main() {
	// 初始化 atomic.Value,存储初始配置
	var config atomic.Value
	config.Store(Config{
		DatabaseURL: "localhost:3306",
		MaxConnects: 10,
		Timeout:     5 * time.Second,
	})

	// 启动一个 goroutine 定时更新配置
	go func() {
		updateCount := 0
		for {
			time.Sleep(2 * time.Second) // 每2秒更新一次配置
			newConfig := Config{
				DatabaseURL: fmt.Sprintf("new_host_%d:3306", updateCount),
				MaxConnects: 10 + updateCount,
				Timeout:     5*time.Second + time.Duration(updateCount)*time.Second,
			}
			config.Store(newConfig) // 原子存储新配置
			updateCount++
		}
	}()

	// 启动多个读取协程,并发读取配置
	for i := 0; i < 3; i++ {
		go func(id int) {
			for {
				time.Sleep(500 * time.Millisecond)
				// 原子读取配置
				currentConfig := config.Load().(Config) // 类型断言
				fmt.Printf("Goroutine %d: Config={URL: %s, MaxConnects: %d, Timeout: %s}\n",
					id, currentConfig.DatabaseURL, currentConfig.MaxConnects, currentConfig.Timeout)
			}
		}(i)
	}

	// 主协程等待
	time.Sleep(10 * time.Second)
}

这里是以一个结构体为例,如果是字符串也是可以的

需要注意一点:

  • Load() 返回 interface{},需强制转换为实际类型

换句话说就是写用 Store,读用 Load,类型要一致

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2396048.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C# NX二次开发-查找连续倒圆角面

在QQ群里有人问怎么通过一个选择一个倒圆角面来自动选中一组倒圆角面。 可以通过ufun函数 UF_MODL_ask_face_type 和 UF_MODL_ask_face_props 可判断处理选择相应的一组圆角面。 代码: Tag[] 查找连续倒圆角面(Tag faceTag) {theUf.Modl.AskFaceType(faceTag, out int typ…

今天遇到的bug

先呈现一下BUG现象。 这主要是一个传参问题&#xff0c;参数一直传不过去。后来我才发现&#xff0c;问题所在。 我们这里用的RquestBody接收参数&#xff0c;所有请求的参数需要用在body体中接收&#xff0c;但是我们用postman&#xff0c;用的是字符串查询方式传参&#x…

长安链智能合约命令解析(全集)

创建命令解析 ./cmc client contract user create \ --contract-namefact \ --runtime-typeWASMER \ --byte-code-path./testdata/claim-wasm-demo/rust-fact-2.0.0.wasm \ --version1.0 \ --sdk-conf-path./testdata/sdk_config.yml \ --admin-key-file-paths./testdata/cryp…

一、OpenCV的基本操作

目录 1、OpenCV的模块 2、OpenCV的基础操作 2.1图像的IO操作 2.2绘制几何图形 2.3获取并修改图像中的像素点 2.4 获取图像的属性 2.5图像通道的拆分与合并 2.6色彩空间的改变 3、OpenCV的算数操作 3.1图像的加法 3.2图像的混合 3.3总结 1、OpenCV的模块 2、OpenCV的基…

裂缝仪在线监测装置:工程安全领域的“实时守卫者”

在基础设施运维领域&#xff0c;裂缝扩展是威胁建筑结构安全的核心隐患之一。传统人工巡检方式存在效率低、时效性差、数据主观性强等局限&#xff0c;而裂缝仪在线监测装置通过技术迭代&#xff0c;实现了对结构裂缝的自动化、持续性追踪&#xff0c;为工程安全评估提供科学依…

【论文精读】2024 ECCV--MGLD-VSR现实世界视频超分辨率(RealWorld VSR)

文章目录 一、摘要二、问题三、Method3.1 Latent Diffusion Model3.2 Motion-guided Diffusion Sampling3.3 Temporal-aware Decoder Fine-tuning 四、实验设置4.1 训练阶段4.2 训练数据 贡献总结 论文全称&#xff1a; Motion-Guided Latent Diffusion for Temporally Consis…

SpringBoot简单体验

1 Helloworld 打开&#xff1a;https://start.spring.io/ 选择maven配置。增加SpringWeb的依赖。 Generate之后解压&#xff0c;代码大致如下&#xff1a; hpDESKTOP-430500P:~/springboot2/demo$ tree ├── HELP.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── s…

2025年通用 Linux 服务器操作系统该如何选择?

2025年通用 Linux 服务器操作系统该如何选择&#xff1f; 服务器操作系统的选择对一个企业IT和云服务影响很大&#xff0c;主推的操作系统在后期更换的成本很高&#xff0c;而且也有很大的迁移风险&#xff0c;所以企业在选择服务器操作系统时要尤为重视。 之前最流行的服务器…

Azure devops 系统之五-部署ASP.NET web app

今天介绍如何通过vscode 来创建一个asp.net web app,并部署到azure 上。 创建 ASP.NET Web 应用 在您的计算机上打开一个终端窗口并进入工作目录。使用 dotnet new webapp 命令创建一个新的 .NET Web 应用,然后将目录切换到新创建的应用。 dotnet new webapp -n MyFirstAzu…

学习路之PHP--easyswoole_panel安装使用

学习路之PHP--easyswoole_panel安装使用 一、新建文件夹二、安装三、改配置地址四、访问 IP:Port 自动进入index.html页面 一、新建文件夹 /www/wwwroot/easyswoole_panel 及配置ftp 解压easyswoole_panel源码 https://github.com/easyswoole-panel/easyswoole_panel 二、安…

【拓扑排序】P6560 [SBCOI2020] 时光的流逝|普及+

本文涉及知识点 C图论 拓扑排序 P6560 [SBCOI2020] 时光的流逝 题目背景 时间一分一秒的过着&#xff0c;伴随着雪一同消融在了这个冬天&#xff0c; 或许&#xff0c;要是时光能停留在这一刻&#xff0c;该有多好啊。 … “这是…我在这个小镇的最后一个冬天了吧。” “嗯…

SSRF 接收器

接收请求 IP.php <?php // 定义日志文件路径 $logFile hackip.txt;// 处理删除请求 if (isset($_POST[delete])) {$ipToDelete $_POST[ip];$lines file($logFile, FILE_IGNORE_NEW_LINES);$newLines array();foreach ($lines as $line) {$parts explode( | , $line);…

FastAPI MCP 快速入门教程

目录 什么是 FastAPI MCP&#xff1f;项目设置1. 初始化项目2. 安装依赖3. 项目结构 编写代码创建主应用文件 运行和测试1. 启动服务器2. 使用 MCP Inspector 测试 什么是 FastAPI MCP&#xff1f; FastAPI MCP 是一个将 FastAPI 应用程序转换为 Model Context Protocol (MCP)…

uni-app学习笔记二十一--pages.json中tabBar设置底部菜单项和图标

如果应用是一个多 tab 应用&#xff0c;可以通过 tabBar 配置项指定一级导航栏&#xff0c;以及 tab 切换时显示的对应页。 在 pages.json 中提供 tabBar 配置&#xff0c;不仅仅是为了方便快速开发导航&#xff0c;更重要的是在App和小程序端提升性能。在这两个平台&#xff…

【Redis】基本命令

Redis命令行客户端 现在我们已经启动了Redis服务&#xff0c;下面将介绍如何使用redis - cli连接、操作Redis服务。客户端和服务端的交互过程如图1 - 3所示。 redis - cli可以使用两种方式连接Redis服务器。 第一种是交互式方式&#xff1a;通过redis - cli -h {host} -p {p…

哈希:闭散列的开放定址法

我还是曾经的那个少年 1.概念 通过其要存储的值与存储的位置建立映射关系。 如&#xff1a;基数排序也是运用了哈希开放定址法的的思想。 弊端&#xff1a;仅适用于数据集中的情况 2.开放定址法 问题&#xff1a;按照上述哈希的方式&#xff0c;向集合插入数据为44&#xff…

Unity-QFramework框架学习-MVC、Command、Event、Utility、System、BindableProperty

QFramework QFramework简介 QFramework是一套渐进式、快速开发框架&#xff0c;适用于任何类型的游戏及应用项目&#xff0c;它包含一套开发架构和大量的工具集 QFramework的特性 简洁性&#xff1a;QFramework 强调代码的简洁性和易用性&#xff0c;让开发者能够快速上手&a…

FPGA实现CNN卷积层:高效窗口生成模块设计与验证

我最近在从事一项很有意思的项目&#xff0c;我想在PFGA上部署CNN并实现手写图片的识别。而本篇文章&#xff0c;是我迈出的第一步。具体代码已发布在github上 模块介绍 卷积神经网络&#xff08;CNN)可以分为卷积层、池化层、激活层、全链接层结构&#xff0c;本篇要实现的&…

LeetCode 3068.最大节点价值之和:脑筋急转弯+动态规划(O(1)空间)

【LetMeFly】3068.最大节点价值之和&#xff1a;脑筋急转弯动态规划&#xff08;O(1)空间&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/find-the-maximum-sum-of-node-values/ 给你一棵 n 个节点的 无向 树&#xff0c;节点从 0 到 n - 1 编号。树以长…

BLIP-2

目录 摘要 Abstract BLIP-2 模型框架 预训练策略 模型优势 应用场景 实验 代码 总结 摘要 BLIP-2 是一种基于冻结的图像编码器和大型语言模型的高效视觉语言预训练模型&#xff0c;由 Salesforce 研究团队提出。它在 BLIP 的基础上进一步优化&#xff0c;通过轻量级…