Go语言开发必知的一个内存模型细节

目录
  • 引言
  • 内存模型定义是什么
  • happens-before 是什么
  • A 不一定 happens-before B
  • Go 语言中的 happens-before
    • 定义
  • Go Channel 实例
  • 例子 1
  • 例子 2
  • 例子 3
  • 例子 4
  • 总结

引言

在日常工作中,如果我们能够了解 Go 语言内存模型,那会带来非常大的作用。这样在看一些极端情况,又或是变态面试题的时候,就能够明白程序运行表现下的很多根本原因了。

当然,靠一篇普通文章讲完 Go 内存模型,不可能。因此今天这篇文章,把重点划在给大家讲解 Go 语言的 happens-before 原则这 1 个细节。

内存模型定义是什么

既然要了解 happens-before 原则,我们得先知道 The Go Memory Model(Go 内存模型)定义的是什么,官方解释如下:

The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.

在 Go 内存模型规定:“在一个 goroutine 中读取一个变量时,可以保证观察到不同 goroutine 中对同一变量的写入所产生的值” 的条件。

这是学习后续知识的一个大前提。

happens-before 是什么

Happens Before 是一个专业术语,与 Go 语言没有直接关系,也就是并非是特有的。用大白话来讲,其定义是:

在一个多线程程序中,假设存在 A 和 B 两个操作,如果 A 操作在 B 操作之前发生(A happens-before B),那么 A 操作对内存的影响将会对执行 B 的线程可见。

A 不一定 happens-before B

从 happens-before 定义来看,我们可以反过来想。那就是:

在同一个(相同)线程中,如果都执行 A 和 B 操作,并且 A 的声明一定在 B 之前,那么 A 一定先于(happens-before)B 发生。

以下述 Go 代码例子:

var A int
var B int
func main() {
 A = B + 1  (1)
 B = 1      (2)
}

该代码是在同一个 main goroutine,全局变量 A 在变量 B 之前声明。

在 main 函数中,代码行 (1),也在代码行 (2) 之前。因此我们可以得出 (1) 一定会在 (2) 前执行,对吗?

答案是:错误的,因为 A happens-before B 并不意味着 A 操作一定会在 B 操作之前发生。

实际上在编译器中,上述代码在汇编的真正执行顺序如下:

0x0000 00000 (main.go:7) MOVQ "".B(SB), AX
 0x0007 00007 (main.go:7) INCQ AX
 0x000a 00010 (main.go:7) MOVQ AX, "".A(SB)
 0x0011 00017 (main.go:8) MOVQ $1, "".B(SB)
  • (2):加载 B 到寄存器 AX。
  • (2):进行 B = 1 赋值,在代码中执行为 INCQ 自增。
  • (1):将寄存器 AX 中值加上 1 后赋值给 A。

通过上述分析,我们可以得知。在代码行 (1) 在 (2) 之前,但确实 (2) 比 (1) 更早执行。

那么这是不是意味着违反了 happens-before 的设计原则,毕竟这可是同个线程里的操作,Go 编译器有 BUG?

其实不然,因为对 A 的赋值实质上对 B 的赋值没有影响。所以并没有违反 happens-before 的设计原则。

Go 语言中的 happens-before

在 《The Go Memory Model》 中,给出了 Go 语言中 Happens Before 的明确语言定义。

以下术语将会在介绍中用到:

  • 变量 v:一个指代性的变量,用于示例演示。
  • 读 r:代表读操作。
  • 写 w:代表写操作。

定义

在满足如下两点条件下,允许对变量 v 的读 r 观察对 v 的写 w:

  • r 在 w 之前没有发生。
  • 没有其他写到 v 的 w' 发生在 w 之后但在 r 之前。

为了保证变量 v 的读 r 观察到对 v 的特定写 w,确保 w 是唯一允许 r 观察的写。

因此如果以下两点都成立,就能保证 r 能观察到 w :

  • w 发生在 r 之前。
  • 对共享变量 v 的任何其他写入都发生在 w 之前或 r 之后。

这看起来比较生涩,接下来我们以《The Go Memory Model》 中具体的 channel 例子来进行进一步说明,会更好理解一些。

Go Channel 实例

在 Go 语言中提倡不要通过共享内存来进行通讯;相反,应当通过通讯来共享内存:

Do not communicate by sharing memory; instead, share memory by communicating.

因此在 Go 工程中,Channel 是一个非常常用的语法。在原则上其需要遵守:

  • 一个 channel 上的发送是在该 channel 的相应接收完成之前发生的。
  • channel 的关闭发生在接收之前,因为通道被关闭而返回一个零值。
  • 一个无缓冲 channel 的接收发生在该 channel 的发送完成之前。
  • 一个容量为 C 的 channel 上,第 k 次接收发生在该 channel 的第 k+C 次发送完成之前。

接下来根据这四条原则,我们逐一给出例子,用于学习和理解。

例子 1

Go channel 例子 1,你认为输出的结果是什么。如下:

var c = make(chan int, 10)
var a string
func f() {
 a = "炸煎鱼"   (1)
 c <- 0        (2)
}
func main() {
 go f()
 <-c           (3)
 print(a)      (4)
}

答案是空字符串吗?

程序最终结果是正常输出 “炸煎鱼” 的,原因如下:

  • (1) happens-before (2) 。
  • (4) happens-after (3)。

当然,最后 (1) 写入变量 a 的操作,必然 happens-before 于 (4) print 方法,因此正确的输出了 “炸煎鱼”。

能够满足 “一个 channel 上的发送是在该 channel 的相应接收完成之前发生的”。

例子 2

主要是确保了关闭管道时的行为。只需要在前面的例子中,替换 c <- 0 成 close(c) 就能够产生具有相同的行为保证的程序。

能够满足 “channel 的关闭发生在接收之前,因为通道被关闭而返回一个零值”。

例子 3

Go channel 例子 3,你认为输出的结果是什么。如下:

var c = make(chan int)
var a string
func f() {
 a = "煎鱼进脑子了"    (1)
 <-c                 (2)
}
func main() {
 go f()
 c <- 0              (3)
 print(a)            (4)
}

答案是空字符串吗?

程序最终结果是正常输出 “煎鱼进脑子了” 的,原因如下:

  • (2) happens-before (3)。
  • (1) happens-before (4)。

能够满足 “一个无缓冲 channel 的接收发生在该 channel 的发送完成之前”。

如果我们把无缓冲改为 make(chan int, 1),也就是带缓冲的 channel,则无法保证正常的输出 “煎鱼进脑子了”。

例子 4

Go channel 例子 4,这个程序为工作列表中的每个条目启动一个 goroutine,但 goroutine 使用 channel 进行协调,以确保每次最多只有三个工作函数在运行。

代码如下:

var limit = make(chan int, 3)
func main() {
 for _, w := range work {
  go func(w func()) {
   limit <- 1
   w()
   <-limit
  }(w)
 }
 select{}
}

能够满足 “一个容量为 C 的 channel 上,第 k 次接收发生在该 channel 的第 k+C 次发送完成之前”。

总结

在本文中,我们针对 happens-before 原则进行了基本的说明。同时结合 Go 语言中实际的 happens-before 和 happens-after 的场景进了展示和讲解。

实际上,在日常的开发工作中,happens-before 原则基本已经深入到潜意识中,就跟设计模式一样。会不知觉就应用到,但是若我们希望更进一步的对 Go 语言等内存模型就行研究和理解,就必须对这个基本理念有所认知。

参考

Golang 内存模型详解(一)

Go内存模型

以上就是Go语言开发必知的一个内存模型细节的详细内容,更多关于Go语言内存模型细节的资料请关注我们其它相关文章!

(0)

相关推荐

  • go语言中切片与内存复制 memcpy 的实现操作

    Go 语言原则上不支持内存的直接操作访问,但是提供了切片功能. 最初我以为切片就是动态数组,实际程序设计过程中发现,切片是提供数组一个内存片段的一个合法的手段,利用切片功能,实际上我们可以自由访问数组的任何一个片段,因而可以借助 copy 函数,实现内存复制. 不同类型之间的数据复制,可以借助 unsafe 取出变量地址,类型转换为数组后,利用数组切片,实现内存复制. 不罗嗦了,示例代码如下: package main import ( "fmt" "unsafe"

  • Go 内存分配管理

    目录 一.问题引入 二.几个基本概念 1.RSS,VSS解释 三.Go内存管理机制 1.Goruntime内存延迟归还 2.强制归还内存 一.问题引入 现象:在实际工作项目中,golang项目经常内存报警,现象为在流量增大,或传入很大文件的情况下算法worker内存降低到一定限度之后(100Mb左右),过一段时间才能内存才能自动恢复. Go内存管理机制,Go自己本身会管理内存,释放的内存不会立即归还给操作系统,在一定时间之后才会归还给操作系统,过早的释放内存归还给操作系统会降低性能. 内存泄漏,

  • 详解Go内存模型

    介绍 Go 内存模型规定了一些条件,在这些条件下,在一个 goroutine 中读取变量返回的值能够确保是另一个 goroutine 中对该变量写入的值.[翻译这篇文章花费了我 3 个半小时 ] Happens Before(在-之前发生) 在一个 goroutine 中,读操作和写操作必须表现地就好像它们是按照程序中指定的顺序执行的.这是因为,在一个 goroutine 中编译器和处理器可能重新安排读和写操作的执行顺序(只要这种乱序执行不改变这个 goroutine 中在语言规范中定义的行为)

  • golang切片内存应用技巧详解

    在 Go 语言中切片是使用非常频繁的一种聚合类型,它代表变长的序列,底层引用一个数组对象.一个切片由三个部分构成:指针.长度和容量.指针指向该切片自己第一个元素对应的底层数组元素的内存地址. 切片的类型声明如下: type slice struct { array unsafe.Pointer len int cap int } 多个切片之间可以共享底层数组的数据,并且引用的数组区间可能重叠.利用切片 的这个特性我们可以在原有内存空间中对切片进行反转.筛选和去重等操作,这样就不用声明一个指向新内

  • 记一次django内存异常排查及解决方法

    起因 Django 作为 Python著名的Web框架,相信很多人都在用,自己工作中也有项目项目在用,而在最近几天的使用中发现,部署Django程序的服务器出现了内存问题,现象就是运行一段时间之后,内存占用非常高,最终会把服务器的内存耗尽,对于Python项目出现内存问题,自己之前处理过一次,所以并没有第一次解决时的慌张,自己之前把解决方法也整理了:https://www.jb51.net/article/151604.htm 但是事情似乎并没有我想的那么简单,自己尝试用之前的的方法tracem

  • Go语言开发必知的一个内存模型细节

    目录 引言 内存模型定义是什么 happens-before 是什么 A 不一定 happens-before B Go 语言中的 happens-before 定义 Go Channel 实例 例子 1 例子 2 例子 3 例子 4 总结 引言 在日常工作中,如果我们能够了解 Go 语言内存模型,那会带来非常大的作用.这样在看一些极端情况,又或是变态面试题的时候,就能够明白程序运行表现下的很多根本原因了. 当然,靠一篇普通文章讲完 Go 内存模型,不可能.因此今天这篇文章,把重点划在给大家讲解

  • Android开发必知 九种对话框的实现方法

    在开发过程中,与用户交互式免不了会用到对话框以实现更好的用户体验,所以掌握几种对话框的实现方法还是非常有必要的.在看具体实例之前先对AlertDialog做一个简单介绍.AlertDialog是功能最丰富.实践应用最广的对话框,它可以生成各种内容的对话框.但实际上AlertDialog生成的对话框总体可分为以下4个区域:图标区.标题区.内容区.按钮区. 这里总结了九种对话框的实现方法,有需要的朋友可以来学习下了 除了popupwindow实现稍微麻烦一点,其他形似都相对简单,熟悉2便即可 直接上

  • go语言开发环境安装及第一个go程序(推荐)

    Go(又称Golang)是Google开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言. 下载Go语言开发包 大家可以在Go语言官网(https://golang.google.cn/dl/)下载 Windows 系统下的Go语言开发包,如下图所示. 安装Go语言开发包 双击我们下载好的Go语言开发包即可启动安装程序,如下图所示,这是Go语言的用户许可协议,无需管它,直接勾选"I accept ..."然后点击"Next"即可. Go语言开发包的安

  • Web开发必知Javascript技巧大全

    JavaScript是一个绝冠全球的编程语言,可用于Web开发.移动应用开发(PhoneGap.Appcelerator).服务器端开发(Node.js和Wakanda)等等.JavaScript还是很多新手踏入编程世界的第一个语言.既可以用来显示浏览器中的简单提示框,也可以通过nodebot或nodruino来控制机器人.能够编写结构清晰.性能高效的JavaScript代码的开发人员,现如今已成了招聘市场最受追捧的人. 在这篇文章里,我将分享一些JavaScript的技巧.秘诀和最佳实践,除了

  • 前端开发必知的15个jQuery小技巧

    下面这些简单的小技巧能够帮助你玩转jQuery. 1.返回顶部按钮 通过使用jQuery中的animate 和scrollTop 方法,不用插件就可以创建一个滚动到顶部的简单动画: // Back to top $('.top').click(function (e) { e.preventDefault(); $('html, body').animate({scrollTop: 0}, 800); }); <!-- Create an anchor tag --> <a class=

  • Java内存模型JMM详解

    Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性.是否可以重排序等问题的无关具体平台的统一的保证.(可能在术语上与Java运行时内存分布有歧义,后者指堆.方法区.线程栈等内存区域). 并发编程有多种风格,除了CSP(通信顺序进程).Actor等模型外,大家最熟悉的应该是基于线程和锁的共享内存模型了.在多线程编程中,需要注意三类并发问题: ·原子性 ·可见性 ·重排序 原子性涉及到,一个线程执行一个复合操作的时候,其他线程是否能够看

  • 关于C++虚继承的内存模型问题

    1.前言 C++虚继承的内存模型是一个经典的问题,其具体实现依赖于编译器,可能会出现较大差异,但原理和最终的目的是大体相同的.本文将对g++中虚继承的内存模型进行详细解析. 2.多继承存在的问题 C++的多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员.从概念上来讲这是非常简单的,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个,比如典型的是菱形继承,如图2-1所示: 图2-1 菱形继承 在图2-1中,类A派生出类B和类C,类D继承自类

  • Java语言的11大特点(Java初学者必知)

    Java简介 Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承.指针等概念,因此Java语言具有功能强大和简单易用两个特征. Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程  . Java具有简单性.面向对象.分布式.健壮性.安全性.平台独立与可移植性.多线程.动态性等特点 Java可以编写桌面应用程序.Web应用程序.分布式系统和嵌入式系统应用程序等 . Java是一种简单的,面向对

  • 经典的20道AJAX面试题(必知必会)

    1.什么是AJAX,为什么要使用Ajax(请谈一下你对Ajax的认识) 什么是ajax: AJAX是"Asynchronous JavaScript and XML"的缩写.他是指一种创建交互式网页应用的网页开发技术. Ajax包含下列技术: 基于web标准(standards-basedpresentation)XHTML+CSS的表示: 使用 DOM(Document ObjectModel)进行动态显示及交互: 使用 XML 和 XSLT 进行数据交换及相关操作: 使用 XMLH

  • JavaScript必知必会(九)function 说起 闭包问题

    function 函数格式 function getPrototyNames(o,/*optional*/ a) { a = a || []; for(var p in o) { a.push(p); } return a; } caller func.caller 返回函数调用者 function callfunc() { if(callfunc.caller) { alert(callfunc.caller.toString()); }else { alert("没有函数调用");

随机推荐