返回顶部
首页 > 资讯 > 后端开发 > GO >Golang内存模型实例源码分析
  • 825
分享到

Golang内存模型实例源码分析

2023-07-05 19:07:56 825人浏览 泡泡鱼
摘要

这篇文章主要介绍“golang内存模型实例源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Golang内存模型实例源码分析”文章能帮助大家解决问题。1. 简介(Introduction)Go

这篇文章主要介绍“golang内存模型实例源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Golang内存模型实例源码分析”文章能帮助大家解决问题。

1. 简介(Introduction)

Go 内存模型涉及到多个 Go协程之间对同一个变量的读写。

假如有一个变量,其中一个 Go协程(a) 写这个变量,另一个 Go协程(b) 读这个变量;Go 内存模型定义了什么情况下 Go协程(b) 能够确保读取到由 Go协程(a) 写入的值。

2. 建议(Advice)

  • 如果多协程并发修改数据,必须保证各个步骤串行执行(序列化访问)。

  • 为了串行执行,可以使用 channel 或其他同步原语( 如 syncsync/atomic 两个包里的那些)来保护被共享的数据。

3. 发生在…之前(Happens Before)

除了重排序需要理解,其余概念其实没那么重要,看后面的例子就懂了。

3.1 重排序

当只有一个 Go协程时,对同一个变量的读写必然是按照代码编写的顺序来执行的。对于多个变量的读写,如果重新排序不影响代码逻辑的正常执行,编译器和处理器可能会对多个变量的读写过程重新排序。

比如对于 a = 1; b = 2 这两个语句,在同一个 Go协程里先执行 哪个其实是没有区别的,只要最后执行结果正确就行。

a := 1//1
b := 2//2
c := a + b //3

但是,因为重新排列执行顺序的情况的存在,会导致**某个 Go协程所观察到的执行顺序可能与另一个 Go协程观察到的执行顺序不一样。**可能另一个 Go协程 观察到的事实是 b 的值先被更新,而 a 的值被后更新。

3.2 happens-before

为了表征读写需求,我们可以定义 happens-before,用来表示 Go 语言中某一小段内存命令的执行顺序。

  • 如果事件 e1 发生在事件 e2 之前,此时我们就认为 e2 发生在 e1 之后。

  • 如果事件 e1 既不发生在事件 e2 之前,也不发生在 e2 之后,此时我们就认为 e1 和 e2 同时发生(并发)(并发 ≠ 并行)。

3.3 规则

在只有一个 Go协程的内部,happens-before的顺序就是代码显式定义的顺序。当 Go协程 不仅仅局限在一个的时候,存在下面两个规则:

如果存在一个变量 v,下面的两个条件都满足,则读操作 r 允许观察到(可能观察到,也可能观察不到)写操作 w 写入的值。

  • r 不在 w 之前发生;

  • 不存在其他的 w’w 之后发生,也不存在 w’r 之前发生。

为了保证读操作 r 读取到的是写操作 w 写入的值,需要确保 w 是唯一允许被 r 观察到的写操作。如果下面的两个条件都满足,则 r 保证能够观察到 w 写入的值:

  • w 发生在 r 之前;

  • 其他对共享变量 v 的写操作要么发生在 w 之前,要么发生在 r 之后。

规则二的条件比规则一的条件更为严格,它要求没有其他的写操作和 w、r 并发地发生。

在一个 Go协程 里是不存在并发的,因此规则一和规则二是等效的:读操作 r 可以观察到最近一次写操作 w 写入的值。

但是,当多个协程访问一个共享变量时,就必须使用同步事件来构建 happens-before 的条件,从而保证读操作观察到的一定是想要的写操作。

在内存模型中,变量 v 的零值初始化操作等同于一个写操作。

如果变量的值大于单机器字(CPU 从内存单次读取的字节数),那么 CPU 在读和写这个变量的时候是以一种不可预知顺序的多次执行单机器字的操作,这也是 sync/atomic 包存在的价值。

4. 同步(Synchronization)

4.1 初始化(Initialization)

程序的初始化是在一个单独的 Go协程 中进行的,但是这个协程可以创建其他的 Go协程 并且二者并发执行。

每个包都允许有一个 init 函数,当这个包被导入时,会执行该包的这个 init 函数,做一些初始化任务。

  • 如果一个包 p 导入了包 q, 那么 qinit 函数的执行发生在 p的所有 init 函数的执行之前。(即包的引用链)

  • 函数 main.main 的执行发生在所有的 init 函数执行完成之后。

4.2 Go协程的创建(Goroutine creation)

通过 go 语句启动新的 Go协程这个动作,发生在新的 Go协程的执行之前。比如下面的例子:

var a stringfunc f() {  print(a)}func hello() {  a = "hello, world"  go f()}

调用函数 hello 会在调用后的某个时间点打印 “hello, world” ,这个时间点可能在 hello 函数返回之前,也可能在 hello 函数返回之后。

4.3 Go协程的销毁(Goroutine destruction)

Go协程的退出无法确保发生在程序的某个事件之前。比如下面的例子:

var a stringfunc hello() {    go func() { a = "hello" }()    print(a)}

其中 a 的赋值语句没有任何的同步措施,因此无法保证被其他任意的 Go 协程(例如 hello 函数本身)观察到这个赋值事件的存在。

一些激进的编译器可能会在编译阶段删除上面代码中的整个 go 语句。

如果某个 Go协程 里发生的事件必须要被另一个 Go协程 观察到,需要使用同步机制进行保证,比如使用或者信道(channel)通信来构建一个相对的事件发生顺序。

4.4 信道通信(Channel communication)

这部分介绍通过 channel 实现并发顺序控制。

缓存channel

信道通信是多个 Go协程 间事件同步的主要方式。在某个特定的信道上发送一个数据,则对应地可以在这个信道上接收一个数据,一般情况下是在不同的 Go协程 间发送与接收。

规则一:在某个信道上发送数据的事件发生在相应的接收事件之前。

即一定是先发送数据,才能接收到数据这个顺序。

var c = make(chan int, 10)var a stringfunc f() {  a = "hello, world"  c <- 0}func main() {  go f()  <-c  print(a)}

上面这段代码保证了 `hello, world` 的打印。因为信道的写入事件 `c <- 0` 发生在读取事件 `<-c` 之前,而 `<-c` 发生在 `print(a)`之前。信道未被读取时协程会阻塞。

规则二:信道的关闭事件发生在从信道接收到零值(由信道关闭触发)之前。

即一定是先关闭 channel,才能接收到零值。

在前面的例子中,可以使用 close(c) 来替代 c <- 0 语句来保证同样的效果。

无缓存 channel

规则三:对于没有缓存的信道,数据的接收事件发生在数据发送完成之前。

即信道容量为0时,只有发送的信息被读取了才算发送成功,否则阻塞。

比如下面的代码(类似上面给出的代码,但是使用了没有缓存的信道,且发送和接收的语句交换了一下):

var c = make(chan int) //容量为0,无缓存var a stringfunc f() {  a = "hello, world"  <-c}func main() {  go f()  c <- 0  print(a)}

上面这段代码依然可以保证可以打印 `hello, world`。因为信道的写入事件 `c <- 0` 发生在读取事件 `<-c` 之前,而 `<-c` 发生在写入事件 `c <- 0` 完成之前,同时写入事件 `c <- 0` 的完成发生在 `print` 之前。

上面的代码,如果信道是带缓存的(比如 `c = make(chan int, 1)`),程序将不能保证会打印出 `hello, world`,它可能会打印出空字符串,也可能崩溃退出,或者表现出一些其他的症状。

规则抽象

规则四:对于容量为 C 的信道,接收第 k 个元素的事件发生在第 k+C 个元素的发送之前。

规则四是规则三在带缓存的信道上的推广。

它使得带缓存的信道可以模拟出计数信号量:**信道中元素的个数表示活跃数,信道的容量表示最大的可并发数;发送一个元素意味着获取一个信号量,接收一个元素意味着释放这个信号量。**这是一种常见的限制并发的用法。

下面的代码给工作列表中的每个入口都开启一个 Go协程,但是通过配合一个固定长度的信道保证了同时最多有 3 个运行的工作(最多 3 个并发)。

var limit = make(chan int, 3)func main() {  for _, w := range work {    go func(w func()) {      limit <- 1  // channel里达到3个即阻塞      w()      <-limit  // 取出后channel里小于3个即可继续    }(w)  }  select{}}

5. 锁

sync 实现了两类锁数据类型,分别是 sync.Mutexsync.RWMutex,即互斥锁和读写锁。

规则一:对于类型为 sync.Mutexsync.RWMutex 的变量 l,如果存在 n 和 m 且满足 n < m,则 l.Unlock() 的第 n 次调用返回发生在l.Lock() 的第 m 次调用返回之前。

即先解开上一次锁才能上这一次锁。

比如下面的代码:

var l sync.Mutexvar a stringfunc f() {  a = "hello, world"  l.Unlock()}func main() {  l.Lock()  go f()  l.Lock()  print(a)}

上面这段代码保证能够打印 `hello, world`。`l.Unlock()`的第 1 次调用返回(在函数 f 内部)发生在 `l.Lock()` 的第 2 次调用返回之前,后者发生在 `print` 之前。

规则二:存在类型 sync.RWMutex 的变量 l,如果 l.RLock 的调用返回发生在 l.Unlock 的第 n 次调用返回之后,那么其对应的 l.RUnlock 发生在 l.Lock 的第 n+1 次调用返回之前。

即读锁可以上多次,但是只要没有全解开就不能上写锁,写锁只能上一个,不解开读写锁都不能上。

6. 单次运行

sync 还提供了 Once 类型用来保证多协程的初始化的安全

多个 Go协程 可以并发执行 once.Do(f) 来执行函数 f, 且只会有一个 Go协程会运行 f(),其他的 Go 协程会阻塞到 f() 运行结束(不再执行 f,但能得到运行结果)

规则一:函数 f()once.Do(f) 的单次调用返回发生在其他所有的 once.Do(f) 调用返回之前。

比如下面的代码:

func setup() {    time.Sleep(time.Second * 2) //1    a = "hello, world"    fmt.Println("setup over") //2}func doprint() {    once.Do(setup) //3    fmt.Println(a) //4    wg.Done()}func twoprint() {    go doprint()    go doprint()}func main() {    wg.Add(2)    twoprint()    wg.Wait()}

setup over
hello, world
hello, world

  • 上面代码使用wg sync.WaitGroup等待两个goroutine运行完毕,由于 setup over只输出一次,所以setup方法只运行了一次

  • 函数 setup 函数的执行返回发生在所有的 print 调用之前,同时会打印出两次 hello, world,即当一个goroutine在执行setup方法时候,另外一个在阻塞。

7. 不正确的同步方式

7.1 案例一

对某个变量的读操作 r 一定概率可以观察到对同一个变量的并发写操作 w,但是即使这件事情发生了,也并不意味着发生在 r 之后的其他读操作可以观察到发生在 w 之前的其他写操作。(这里的先后指的是代码里面声明的操作的先后顺序,而不是实际执行时候的)

比如下面的代码:

var a, b intfunc f() {  a = 1  b = 2}func g() {  print(b)  print(a)}func main() {  go f()  g()}

上面的代码里函数 g 可能会先打印 2(b的值),然后打印 0(a的值)。可能大家会认为既然 b 的值已经被赋值为 2 了,那么 a 的值肯定被赋值为 1 了,但事实是两个事件的先后在这里是没有办法确定的,因为编译器会改变执行顺序。

上面的事实可以证明下面的几个常见的错误。

7.2 案例二

双重检查锁定尝试避免同步带来的开销。比如下面的例子,twoprint 函数可能会被错误地编写为:

var a stringvar done boolfunc setup() {  a = "hello, world"  done = true}func doprint() {  if !done {    once.Do(setup)  }  print(a)}func twoprint() {  go doprint()  go doprint()}

doprint 函数中,观察到对 done 的写操作并不意味着能够观察到对 a 的写操作。上面的写法依然有可能打印出空字符串。

7.3 案例三

另一个常见的错误用法是对某个值的循环检查,比如下面的代码:

var a stringvar done boolfunc setup() {    a = "hello, world"    done = true}func main() {    go setup()    for !done {    }    print(a)}

和上一个例子类似,main函数中观察到对 done 的写操作并不意味着可以观察到对 a 的写操作,因此上面的代码依然可能会打印出空字符串。

更糟糕的是,由于两个 Go协程之间缺少同步事件,main 函数甚至可能永远无法观察到对 done 变量的写操作,导致 main 中的 for 循环永远执行下去。

上面这个错误有一种变体,如下面的代码所示:

type T struct {  msg string}var g *Tfunc setup() {  t := new(T)  t.msg = "hello, world"  g = t}func main() {  go setup()  for g == nil {  }  print(g.msg)}

上面的代码即使 main 函数观察到 g != nil并且退出了它的 for 循环,依然没有办法保证它可以观察到被初始化的 g.msg 值。

避免上面几个错误用法的方式是一样的:显式使用同步语句。

关于“Golang内存模型实例源码分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注编程网GO频道,小编每天都会为大家更新不同的知识点。

您可能感兴趣的文档:

--结束END--

本文标题: Golang内存模型实例源码分析

本文链接: https://lsjlt.com/news/354514.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Golang内存模型实例源码分析
    这篇文章主要介绍“Golang内存模型实例源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Golang内存模型实例源码分析”文章能帮助大家解决问题。1. 简介(Introduction)Go ...
    99+
    2023-07-05
  • Java内存模型的示例分析
    这篇文章主要为大家展示了“Java内存模型的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Java内存模型的示例分析”这篇文章吧。1. 为什么要有内存模型?要想回答这个问题,我们需要先弄...
    99+
    2023-06-29
  • jvm中java内存模型的示例分析
    这篇文章主要介绍了jvm中java内存模型的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一、java内存模型和java内存结构有什么区别 1、java内存...
    99+
    2023-06-19
  • java内存模型讨论及案例分析
    常用内存选项 -Xmx: 最大堆大小 -Xms:最小堆大小 -Xss :线程堆栈大小,默认1M 生产环境最好保持 Xms = Xmx java内存研究 内存布局 可见: 堆大小 = 新生代 + 老年代,新生代=E+From Survivo...
    99+
    2023-08-30
    java 开发语言
  • nginx内存池源码分析
    本篇内容主要讲解“nginx内存池源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“nginx内存池源码分析”吧!内存池概述    内存池是在真正使用内存之前,...
    99+
    2023-06-25
  • JVM系列之内存模型的示例分析
    这篇文章主要介绍JVM系列之内存模型的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1. 内存模型和运行时数据区这一章学习java虚拟机内存模型(Java Virtual machine menory mod...
    99+
    2023-06-15
  • Redis内存对像模型分析
    本篇内容介绍了“Redis内存对像模型分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Redis内存统计...
    99+
    2024-04-02
  • 详细分析Java内存模型
    目录一、为什么要学习并发编程二、为什么需要并发编程三、从物理机中得到启发四、Java 内存模型五、原子性5.1、什么是原子性5.2、如何保证原子性六、可见性6.1、什么是可见性6.2...
    99+
    2024-04-02
  • pytorch实践线性模型3d源码分析
    这篇文章主要介绍“pytorch实践线性模型3d源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“pytorch实践线性模型3d源码分析”文章能帮助大家解决问题。y = wx +b通过meshg...
    99+
    2023-07-06
  • Java内存模型之重排序的示例分析
    小编给大家分享一下Java内存模型之重排序的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、数据依赖性如果两个操作访问同一个变量,而且这两个操作中有一个操作为写操作,此时这两个操作之间存在数据依赖性。数据依赖性分...
    99+
    2023-06-15
  • C++浅析内存分区模型概念与示例
    目录初识C++内存分区模型程序运行前内存分区代码示例初识C++内存分区模型 在了解内存分区之前,我们先来聊一聊为什么要进行内存分区。在进行了内存分区之后,在不同的区域存放的数据,会有...
    99+
    2024-04-02
  • Python内建类型int源码分析
    今天小编给大家分享一下Python内建类型int源码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。问题:对于C语言,下面...
    99+
    2023-06-30
  • Python内建类型float源码分析
    这篇文章主要介绍“Python内建类型float源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python内建类型float源码分析”文章能帮助大家解决问题。1 回顾float的基础知识1....
    99+
    2023-06-30
  • Python内建类型str源码分析
    这篇文章主要讲解了“Python内建类型str源码分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Python内建类型str源码分析”吧!1 Unicode计算机存储的基本单位是字节,由8...
    99+
    2023-06-30
  • Python内建类型dict源码分析
    本篇内容主要讲解“Python内建类型dict源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python内建类型dict源码分析”吧!深入认识Python内建类型&mdash;&...
    99+
    2023-07-05
  • GoLang OS包及File类型源码分析
    这篇文章主要介绍“GoLang OS包及File类型源码分析”,在日常操作中,相信很多人在GoLang OS包及File类型源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”GoLan...
    99+
    2023-07-05
  • Java内存模型顺序一致性的示例分析
    小编给大家分享一下Java内存模型顺序一致性的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!简介:顺序一致性内存模型是一个理论参考模型,处理器的内存模型和...
    99+
    2023-06-25
  • golang内存模型是什么
    Golang的内存模型是一种并发内存模型,它定义了在不同Goroutine之间共享数据的行为。在Golang的内存模型中,每个Gor...
    99+
    2023-10-20
    golang
  • Python内建类型bytes实例代码分析
    这篇文章主要讲解了“Python内建类型bytes实例代码分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Python内建类型bytes实例代码分析”吧!1 bytes和str之间的关系不...
    99+
    2023-06-30
  • Golang数据类型实例代码比较分析
    这篇文章主要讲解了“Golang数据类型实例代码比较分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Golang数据类型实例代码比较分析”吧!分类说明是否能比较说明基本类型整型( int/...
    99+
    2023-07-06
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作