首页 > 系统相关 >深入理解 Go 语言原子内存操作

深入理解 Go 语言原子内存操作

时间:2024-08-19 22:26:22浏览次数:14  
标签:goroutine 原子 done 内存 func Go 操作

        原子内存操作提供了实现其他同步原语所需的低级基础。一般来说,你可以用互斥体和通道替换并发算法的所有原子操作。然而,它们是有趣且有时令人困惑的结构,应该深入了解它们是如何工作的。如果你能够谨慎地使用它们,那么它们完全可以成为代码优化的好工具,而不会增加复杂性。

1. 原子内存操作的内存保证

        为什么我们需要单独的函数来进行原子内存操作?如果我们写入一个变量,其大小小于或等于机器字长( 现代计算机的机器字长一般都 8 位的整数倍,如 8 位、16 位等,这是由 int 类型定义的东西),例如 a = 1 ,这不就是原子的吗?

        Go 内存模型实际上保证了写操作是原子的,但是它并不能保证其他 goroutine 何时会看到该写操作的效果。

        让我们仔细分析这句话的含义。第一层意思是说,如果你从一个 goroutine 中写入与机器字长(即int) 大小相同的共享内存位置并从另一个 goroutine 中读取它,那么即使存在竞争,你也不会观察到写入操作之前的值或写入操作之后的值(并非所有语言都如此)。这意味着,如果写操作大于机器字长,那么读取该值的 goroutine 可能会看到底层对象处于不一致的状态。例如,string 值包括两个值:指向底层数组的指针和字符串长度。对这些单独的写入操作是原子的,但快速读取操作可能会读取带有 nil 数组但长度非零的字符串。

        这句话的第二层意思是说,编译器可能优化或重新排序代码,或者硬件可能乱序执行内存操作,从而使另一个 goroutine 在预期时间无法看到写入操作的效果。说明这一点的标准示例就是以下内存竞争:

package main

func main() {
	var str string
	var done bool
	go func() {
		str = "Done!"
		done = true
	}()
	for !done {

	}
	fmt.Println(str)
}

        这里就存在内存竞争,因为 str 变量和 done 变量在一个 goroutine 中被写入并在另一个 goroutine 中被读取,但没有显式同步。

该程序有多种可能的结果:

  • 它可以输出 Done ! 。
  • 它可以输出一个空字符串。这意味着主 goroutine 可以看到对 done 的内存写入,但看不到对 srt 的内存写入。
  • 程序可能会挂起。这意味着主 goroutine 看不到对 done 的内存写入。

这就是原子操作发挥作用的地方。以下程序是没有内存竞争的:

func main(){
	var str done atomic.Value
	var done atomic.Bool
	str.Store("")
	go func(){
		str.Store("Done!")
		done.Store(true)
	}()
	for !done.Load(){
	}
	fmt.Println(str.Load())
}

        原子操作的内存保证如下。如果原子内存写入的效果可以通过原子读取观察到,则原子写入发生在原子读取之前。这也保证了以下程序要么输出 1 ,要么不输出任何东西(永远都不会输出 0):

func main(){
	var done atomic.Bool
	var a int
	go func(){
		a = 1
		done.Store(true)
	}()
	if done.Load(){
		fmt.Println(a)
	}
}

        值得一提的是,这里仍然存在竞争条件,但不是内存竞争。根据语句的执行顺序,主 goroutine 可能会也可能不会看到 done 为 true 。但是,如果主 goroutine 看到 done 为 true,那么就可以保证 a = 1 。

        这就是为什么使用原子操作会变得复杂的原因之一:内存排序保证是有条件的。它们永远不会阻塞正在运行的 goroutine,因此你测试原子读取是否返回变量的特定值这一事实并不意味着当 if 语句主体运行时它仍然具有相同的值。这就是为什么在使用原子操作时需要小心。使用它们很容易陷入竞争条件,就像之前的程序这样。

2. 比较和交换操作

        每当你需要测试条件并根据结果采取行动时,你都可以创建竞争条件。例如,尽管使用了原子操作,但以下函数并不能阻止互斥:

var locked sync.Bool

func wrongCriticalSectionExample(){
	if !locked.Load(){
		// 其他 goroutine 现在可以锁定它!
		locked.Store(true)
		defer locked.Store(false)
		// 该 goroutine 进入临界区,但其他 goroutine 也可以
	}
}

        该函数首先测试原子 locked 值是否为 false。两个 goroutine 可以同时执行这条语句,并且看到它为 false,它们都可以进入临界区并将 locked 设置为 true。

        这里需要的包含比较和存储操作的原子操作,也就是比较和交换(compare-and-swap,CAS)操作。正如其名称所暗示的那样,它将比较变量是否具有预期值,如果是,则自动将该值与给定值进行交换。如果变量具有不同的值,则不会发生任何更改。也就是说,CAS 操作是以下形式的,并以原子方式完成:

if *variable == testValue {
	*variable = newValue
	return true
}
return false

        现在你可以真正实现非阻塞互斥体:

func criticalSection(){
	if locked.CompareAndSwap(false,true)
	defer locked.Store(false)
	// 临界区
	}
}

        只有当 locked 为 false 时才会进入临界区。如果是这种情况,那么它会自动将 locked 设置为 true 并进入其临界区;否则,它将跳远临界区并明治继续。因此,这实际上可以用来代替 Mutex.TryLock。

3. 原子的实际用途

3.1 计数器

        原子可以用作高效的并发安全计数器。

        以下程序示例将创建许多 goroutine, 其中每个 goroutine 都会将共享计数器加 1。另一个 goroutine 则循环直至计数器达到 10000。由于这里使用了原子,因此该程序是无竞争的,并且它始终会通过最终输出 10000 来终止。

var count int64

func main() {
	for i := 0,i <10000; i++ {
		go func(){
			atomic.AddInt64(&count,1)
		}()
	}
	
	for {
		v := atomic.LoadInt64(&count)
		fmt.Println(v)
		if v == 10000 {
			break
		}
	}
}
3.2 心跳和进度表

        有时,goroutine 可能会变得无响应或无法按需要快速进行。心跳实用程序和进度表可用于观察此类 goroutine 。有若干种方法可以做到这一点。例如,被观察的 goroutine 可以使用非阻塞发送来宣布进度,或者它可以通过增加由互斥体保护的共享变量来宣布其进度。原子允许我们在没有互斥体的情况下实现共享变量方案。这样做还有一个可以好处是可以被多个 goroutine 观察而无须额外的同步。

        让我们定义一个包含原子值的简单 ProgressMeter 类型:

type ProgressMeter struct {
	progress int64
}

        被观察的 goroutine 使用以下方法来指示其进度。此方法只是自动将进度值递增1:

func (pm *ProgressMeter) Progress(){
	atomic.AddInt64(&pm.progress,1)
}

        Get 方法可以返回进度的当前值。请注意,该负载是原子的,否则就有可能会丢失对变量的原子添加:

func (pm *ProgressMeter) Get() int64 {
	wg.Wait()
}

        可以看到,我们将 cancel 函数传递给了观察者,以便它可以向被观察的 goroutine 发送取消的消息。 

3.3 取消

        我们可以通过关闭通道来发出取消信号。Context 实现可以使用此范式来发出取消和超时信号。使用原子也可以实现简单的取消方案:

func CancelSupport()(cancel func(),isCancelled func() bool){
	v := atomic.Bool{}
	cancel = func(){
		v.Store(true)
	}
	isCancelled = func() bool {
		return v.Load()
	}
	return
}

        CancelSupport 函数返回两个闭包,其中,cancel() 函数可被调用以发出取消信号,而 isCancelled( )函数则可用于检查取消请求是否已注册。这两个闭包共享一个原子 bool 值,这可以按如下方式使用:

func main(){
	cancel,isCanceled := CancelSupport()
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func(){
		defer wg.Done()
		for {
			time.Sleep(100 * time.Millisecond)
			if isCanceled(){
				fmt.Println("Cancelled")
				return
			}
		}
	}()
	time.AfterFunc(5*time.Second,cancel)
		wg.Wait()
}
3.4 检测变化

        假设你有一个可以从多个 goroutine 中更新的共享变量。你读取此变量,执行了一些计算,现在你想要更新它。但是,在你获得副本后,另一个 goroutine 可能已经修改了该变量。因此,当且仅当其他 goroutine 没有更改此变量时,你才可以更新它。

以下代码片段使用比较和交换(CAS)操作对此进行说明:

var sharedValue atomic.Pointer[SomeStruct]

func unpdateSharedValue(){
	myCopy := sharedValue.Load()
	newCopy := computeNewCopy(*myCopy)
	if sharedValue.CompareAndSwap(myCopy,&newCopy){
		fmt.Println("Set value successful")
	} else {
		fmt.Println("Another goroutine modified the value")
	}
}

        这段代码很容易出现竞争,所以你必须小心。SharedValue.Load( )调用以原子方式返回指向共享值的指针。如果另一个 goroutine 修改了指向 *sharedValue 对象的内容,则出现了竞争。仅当所有 goroutine 以原子方式获取指针并复制底层数据结构时,这才有效。然后,我们使用 CAS 写入修改后的副本,但如果另一个 goroutine 表现得更快,则写入操作会失败。

标签:goroutine,原子,done,内存,func,Go,操作
From: https://blog.csdn.net/canglonghacker/article/details/141313054

相关文章

  • Java数组02:数组内存分析、三种初始化方式及特点
    本节内容视频链接:Java数组03:三种初始化及内存分析_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV12J41137hu?p=53&vd_source=b5775c3a4ea16a5306db9c7c1c1486b51.数组内存分析堆:存放new的对象和数组;可以被所有线程共享,不会存放别的对象引用;栈:存放基本变量类型,会包含......
  • C++ 获取Linux 服务器CPU占用率+内存空闲率(亲测绝对可以运行)
    转自:C++获取Linux服务器CPU占用率+内存空闲率(亲测绝对可以运行)-远征i-博客园(cnblogs.com)代码来自网络,部分修改,亲测绝对可用C++:#include<stdio.h>#include<stdlib.h>#include<string.h>#include<iostream>#include<unistd.h>usingnamespacestd;type......
  • 高校爬虫可视化系统-基于python|Django|flask的高校爬虫可视化系统|大学数据抓取与展
    博主介绍:✌十余年IT大项目实战经验、在某机构培训学员上千名、专注于本行业领域✌技术范围:Java实战项目、Python实战项目、微信小程序/安卓实战项目、爬虫+大数据实战项目、Nodejs实战项目、PHP实战项目、.NET实战项目、Golang实战项目。主要内容:系统功能设计、开题报告......
  • CHC5223 Data Structures and Algorithms
    CHC5223DataStructuresandAlgorithms2023-2024-21of6AssignmentValue100%ofCourseworkResitIndividualworkBackgroundThesubwaysystemofacityisanetworkofundergroundorelevatedtrainsthatproviderapidtransitforpassengerswithint......
  • C/C++语言基础--指针三大专题详解2(指针与数组关系,动态内存分配,代码均可)
    本专栏目的更新C/C++的基础语法,包括C++的一些新特性前言指针是C/C++的灵魂,和内存地址相关联,运行的时候速度快,但是同时也有很多细节和规范要注意的,毕竟内存泄漏是很恐怖的指针打算分三篇文章进行讲解,本专题是二,介绍了指针和数组的关系、动态内存如何分配和释放等问题专题......
  • go语言学习过程报错处理-哇哈哈哈
    题记无聊学习ing,思考了下还是学下go语言写免杀木马吧,毕竟在我的学习计划里放了小半年了,上班的时候还没多少自己的时间学习。为什么无聊大家都懂吧,应该会懂的吧。主要还是需要分散下注意力,近期脑子整天都是奇奇怪怪的幻想,太影响人了。当然,还是要加上我喜欢的好句子,......
  • 内存(动态开辟)———C语言
    内存管理: 1.C语言运行时的内存分配2.static关键字1.修饰变量局部变量:        <1>在编译的过程中,会在数据区为该变量开辟空间,如果代码中未对其进行初始化,则系统默认初始化为0。        <2>用static修饰的局部变量,会延长局部变量的生命周期#include<s......
  • 四十、【人工智能】【机器学习】- 梯度下降(Gradient Descent Algorithms)算法模型
     系列文章目录第一章【机器学习】初识机器学习第二章【机器学习】【监督学习】-逻辑回归算法(LogisticRegression)第三章【机器学习】【监督学习】-支持向量机(SVM)第四章【机器学习】【监督学习】-K-近邻算法(K-NN)第五章【机器学习】【监督学习】-决策树(......
  • linux安装go 环境
    嗯,每个人的工作方法不一样,不喜勿喷哈这是我安装的go不是最新的[root@simetra-ecs-01go]#goversiongoversiongo1.19.8linux/amd64[root@simetra-ecs-01go]#首先先去下载golang的安装包,我用的是go1.19.8.linux-amd64.tar.gz解压到/usr/local目录下[root@k8s......
  • 基于django+vue框架的实时新闻推送平台edpjq【开题报告+程序+论文】计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景在信息爆炸的时代,新闻资讯的时效性成为了媒体竞争的关键。随着互联网技术的飞速发展,人们获取新闻的方式已从传统的报纸、电视转向了手机、......