The Go Memory Model(go 内存模型)

[译]https://golang.google.cn/ref/mem

Go内存模型指定了一个条件,在该条件下,可以保证在一个 goroutine 中读取变量,能够获取到另一个不同 goroutine 写入同一变量产生的值。

Introduction

Go内存模型指定了一个条件,在该条件下,可以保证在一个 goroutine 中读取变量,能够获取到另一个不同 goroutine 写入同一变量产生的值。

Advice

如果一个程序要修改被多个 goroutine 同时访问的数据,必须序列化此类访问。

要序列化访问,请使用 channel 操作或其他同步原语(例如syncsync/atomic包中的那些)来保护数据。

如果您必须阅读本文档的其余部分以了解程序的行为,那么您就太聪明了。

别聪明。

Happens Before

在单个 goroutine 中,读取和写入必须表现得好像它们按程序指定的顺序执行。也就是说,只有当重新排序不改变语言规范中定义的 goroutine 的行为时,编译器和处理器才可以对在单个 goroutine 中读取和写入操作的执行进行重新排序。由于这种重新排序,一个 goroutine 观察到的执行顺序可能与另一个 goroutine 感知到的顺序不同。例如,如果一个 goroutine 执行a = 1; b = 2;,另一个 goroutine 可能会在 a 的值更新之前观察到 b 的更新值。

为了指定读取和写入的要求,我们定义 发生之前(hanppen before),Go 程序中内存操作的局部顺序。如果事件 e1 在事件 e2 发生之前(hanppen before),那么我们说事件 e2 在事件 e1 发生之后(hanppen after)。另外,如果 e1 在 e2 之前没有发生并且在 e2 之后没有发生,那么我们说 e1 和 e2 同时发生。

在单个goroutine中,happens-before 顺序是程序表达的顺序。

如果以下两个都成立,则允许变量 v 的读取操作 r 观察到写入操作 w 写入到 v 的值:
1. r 没有发生在 w 之前。
2. 在 w 之后但在 r 之前没有其他写入操作 w‘。

为了保证对变量 v 的读取操作 r 观察到特定写入操作 w 对 v 写入的值,确保 w 是允许读取操作 r 观察到的唯一的写入操作。也就是说,如果以下两个条件都成立,才能保证读取操作 r 能够观察到写入操作 w:
1. w 发生在 r 之前。
2. 任何其他对共享变量 v 的写入操作,要么发生在 w 之前,要么发生在 r 之后。

这组条件比第一组更加严格。它要求没有其他写入与 w 或 r 同时发生。

在单个goroutine中,没有并发,因此这两个定义是等效的:一个读取操作 r 观察最近的写入操作 w 写入 v 的值。

具有零值的 v 的类型的变量 v 的初始化表现为以上存储模型中的写入。

对于大于单个机器字的值的读取和写入操作,表现为以未指定顺序进行的多个 机器字大小的操作。

Synchronization

Initialization

程序的初始化在单个 goroutine 中运行,但该 goroutine 可能会创建其他并发运行的 goroutine。

如果包 p 导入包 q,则 q 的 init 函数在包 p 的任何代码开始之前完成。

函数 main.main 在所有的 init 函数完成后开始执行。

Goroutine creation

启动新 goroutine 的 go 语句发生在该 goroutine 开始执行之前。

例如,在此程序中:

var a string

func f() {
    print(a)
}

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

调用hello将在未来的某个时刻打印“hello,world”(也许在hello返回之后)。

Goroutine destruction

goroutine 的退出不保证在程序中的任何事件之前发生。例如,在此程序中:

var a string

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

对 a 的赋值没有伴随任何同步事件,因此不保证任何其他 goroutine 都能观察到它。事实上,一个激进的编译器可能会删掉整条 go 语句。

如果一个 goroutine 影响必须被另一个 goroutine 观察到,要使用锁或 channel 通信等同步机制来建立相对顺序。

Channel communication

channel 通信是 goroutine 之间同步的主要方法。特定 channel 上的每一个 send 操作都与该 channel 对应的 receive 操作相匹配,通常在不同的 goroutine 中。

channel 的 send 在该 channel 相应的 receive 操作完成之前发生

示例程序:

var c = make(chan int, 10)
var a string

func f() {
    a = "hello, world"
    c <- 0
}

func main() {
    go f()
    <-c
    print(a)
}

保证打印出 "hello, world"。对 a 的写入发生在 c 的 send 之前,即发生在 c 的相应的 receive 完成之前,即发生在print之前。

channel 的关闭发生在因通道已关闭而接收到零值返回之前

在前面的示例中,用close(c)替换c <- 0会产生具有保证同样行为的程序。

无缓冲 channel 的 receive 操作在该 channel 的 send 操作完成之前发生。

示例程序(和上面一样,但是交换了 send 和 receive 语句并且使用了无缓冲的 channel):

var c = make(chan int)
var a string

func f() {
    a = "hello, world"
    <-c
}

func main() {
    go f()
    c <- 0
    print(a)
}

同样保证打印出 "hello, world"。对 a 的写入发生在 c 的 receive 之前,即发生在 c 的相应的 send 完成之前,即发生在print之前。

如果 channel 是有缓冲的,(例如,c = make(chan int, 1)),那么程序将不能保证打印 "hello, world"。(可能会打印空字符串,崩溃或执行其他操作。)

具有容量C的 channel 的第 k 次 receive 操作,在第 k+C 次 send 操作完成之前。
此规则概括了先前的有缓冲的 channel 的规则。它允许用有缓冲的 channel 建立的计数信号量:channel 中的 data 数量对应于当前的使用数量,channel 的容量对应于允许最大同时使用的数量,发送一条 data 来获取信号量,接收一条 data 来释放信号量。这是限制并发数量的常用用法。

该程序为工作列表中的每个条目启动一个 goroutine,但是 goroutine 利用limit这个 channel 来确保一次最多有三个正在运行的work函数。

var limit = make(chan int, 3)

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

Locks

sync包实现了两种锁的类型,sync.Mutexsync.RWMutex

对于任何sync.Mutexsync.RWMutex类型的变量l,并且n < m,第n次调用l.Unock()在第m次调用l.Lock()返回之前发生。
示例程序:

var l sync.Mutex
var a string

func f() {
    a = "hello, world"
    l.Unlock()
}

func main() {
    l.Lock()
    go f()
    l.Lock()
    print(a)
}

保证打印出"hello, world"。第一次调用l.Unlock()(在f()中),在第二次调用l.Lock()(在main()中)返回之前发生,即在print之前发生。

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n+1 to l.Lock.

对于sync.RWMutex类型的变量ll.RLock()的任意调用,有一个 n 使得本次l.RLock()在第 n 次调用l.Unlock()之后发生(返回)并且对应的l.RUnlock()在第 n+1 调用l.Lock()之前发生。

Once

sync包通过使用Once类型,在存在多个 goroutine 的情况下提供了一种安全的初始化机制。多个线程可以对特定的 f 执行nce.Do(f),但是只有一个线程会真正运行f(),并且其他调用会阻塞直到f()返回。

once.Do(f)中对f()的单次调用在任意once.Do(f)的调用之前发生(返回)。
在如下程序中:

var a string
var once sync.Once

func setup() {
    a = "hello, world"
}

func doprint() {
    once.Do(setup)
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

调用twoprint()将只会调用setup()一次。setup 方法将在print之前完成。结果是"hello, world"将被打印两次。

Incorrect synchronization

注意,读取操作 r 可以观察到与r同时发生的写入操作 w 所写的值。即使发生这种情况,也不意味着在 r 之后发生的读取操作将观察到在 w 之前发生的写入操作。
如下程序中:

var a, b int

func f() {
    a = 1
    b = 2
}

func g() {
    print(b)
    print(a)
}

func main() {
    go f()
    g()
}

可能会发生 g 先打印 2 然后打印 0。

这使一些常见的管用语法无效。

双重检查锁 是为了避免同步的开销。
例如,twoprint程序可能被错误的写为:

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func doprint() {
    if !done {
        once.Do(setup)
    }
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

但是这不能保证,在doprint中,观察到done的写入操作意味着同样能观察到对a的写入操作。这个版本可能(错误地)打印空字符串而不是"hello,world"。

另一个不正确的惯用语法是忙着等待一个值,如:

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {
    }
    print(a)
}

像之前一样,不能保证在main中,观察到done的写入操作意味着同样能观察到对a的写入操作,因此这个程序也可能打印出空的字符串。更糟的是,还无法保证main能观察到对done的写入操作,因为两个线程之间没有同步事件。main中的循环无法保证能完成。

这个主题有更微妙的变体,例如这个程序:

type T struct {
    msg string
}

var g *T

func setup() {
    t := new(T)
    t.msg = "hello, world"
    g = t
}

func main() {
    go setup()
    for g == nil {
    }
    print(g.msg)
}

即使main观察到g != nil并且退出循环,无法保证它会观察到g.msg的初始化值。

在所有这些示例中,解决方案是相同的:使用显式的同步。

原文地址:https://www.cnblogs.com/maoqide/p/11258521.html

时间: 2024-10-20 13:57:41

The Go Memory Model(go 内存模型)的相关文章

单例设计模式和Java内存模型

使用双检索的单例设计模式延迟加载存在的问题 这篇文章介绍了使用双检索延迟加载的单例模式存在的问题,以下的代码由于指令重排序可能会无法正常工作. 正常的执行顺序是 执行构造函数 构造函数执行完毕 将新构造的对象赋值到引用 但由于指令的乱序执行,代码的执行顺序可能变为 执行构造函数 将对象赋值到引用 构造函数执行完毕 由此,线程可能获取到一个没有初始化完毕的对象. 1 class Foo { 2 private Helper helper = null; 3 public Helper getHel

浅析java内存模型--JMM(Java Memory Model)

在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步? 在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的. 线程之间通过共享程序公共的状态,通过读-写内存中公共状态的方式来进行隐式的通信.同步指的是程序在控制多个线程之间执行程序的相对顺序的机制,在共享内存模型中,同步是显式的,程序员必须显式指定某个方法/代码块需要在多线程之间互斥执行. 在说Java内存模型之前,我们先说一下Java的内存结构,也就是运行时的数据区域: Java虚拟机在执

图解JAVA内存模型(JMM:JAVA Memory Model)

引言 本文主要说明两个问题:JMM存在的意义是什么?JMM内部的工作原理是什么(重点讲一下并发编程模式下的数据访问一致性问题) . 1.为什么要使用JMM? 当我们刚开始接触JAVA语言的时候,就会被告知JAVA程序是可以实现跨平台运行的(即同一份代码资源可运行在不同的硬件配置下,不同的操作系统下).那么JAVA 是如何在不同的硬件和操作系统内存访问方式存在差异的情况下,实现 同一个Java 程序在各种平台下的运行结果都相同(达到一致的内存访问效果)的呢.靠的就是神奇的JMM. 在这里要牢记两个

java内存模型(Java Memory Model)

内容导航: Java内存模型 硬件存储体系结构 Java内存模型和硬件存储体系之间的桥梁: 共享对象的可见性 竞争条件 Java内存模型规定了JVM怎样与计算机存储系统(RAM)协调工作.JVM是一个虚拟机模型,因此这个模型自然包含一个内存的模型 理解java内存模型对于设计正确的并发程序非常重要.JVM规定了不同线程何时以及怎样能看到那些被共享变量的读写,怎样同步对共享变量的訪问控制. 最初的java内存模型并不完好.所以他在java1.5中被改动了.以下的内存模型在java1.8中仍然使用.

并发编程之java内存模型(Java Memory Model ,JMM)

一.图例 0.两个概念 Heap(堆):运行时的数据区,由垃圾回收负责,运行时分配内存(所以慢),对象存放在堆上 如果两个线程,同时调用同一个变量,怎两个线程都拥有,该对象的私有拷贝 (可以看一下,ThreadLocal:   引用注明出处,https://www.cnblogs.com/xiaonantianmen/p/9151481.html) Stack(栈):存放一些引用变量 二.多cpu情况 0.JVM与物理内存之间的通信. 2.线程之间的通信必须通过主内存(此处则是要考虑synchr

C语言内存模型 (C memory layout)

 一. 内存模型                                                                         1. .text 代码区(code section).由编译器链接器生成的可执行指令,程序执行时由加载器(loader)从可执行文件拷贝到内存中.为了安全考虑,防止别的区域更改代码区数据(即可执行指令),代码区具有只读属性.另一个方面,代码区通常具有可共享性(sharable),即在内存中只有一份代码区,如编译器,假如同时有多个编译任务

java学习:JMM(java memory model)、volatile、synchronized、AtomicXXX理解

一.JMM(java memory model)内存模型 从网上淘来二张图: 上面这张图说的是,在多核CPU的系统中,每个核CPU自带高速缓存,然后计算机主板上也有一块内存-称为主内(即:内存条).工作时,CPU的高速缓存中的数据通过一系列手段来保证与主内的数据一致(CacheCoherence),更直白点,高速缓存要从主内中load数据,处理完以后,还要save回主存. 上图说的是,java中的各种变量(variable)保存在主存中,然后每个线程自己也有自己的工作内存区(working me

C# 内存模型

C# 内存模型 This is the first of a two-part series that will tell the long story of the C# memory model. The first part explains the guarantees the C# memory model makes and shows the code patterns that motivate the guarantees; the second part will detai

java内存模型与线程(转) good

java内存模型与线程 参考 http://baike.baidu.com/view/8657411.htm http://developer.51cto.com/art/201309/410971_all.htm http://www.cnblogs.com/skywang12345/p/3447546.html 计算机的CPU计算能力超强,其计算速度与 内存等存储 和通讯子系统的速度相比快了几个数量级, 数据加载到内存中后,cpu处理器运算处理时,大部分时间花在等待获取去获取磁盘IO.网络通