• Golang内存模型


    Ref: https://golang.org/ref/mem

    1. 简介

    golang内存模型,主要说明了如下问题。在一个goroutine中读取变量,而该变量是由其他goroutine赋值的,这种情况下如何能够安全正确的读取。

    1. 建议

    对于有多个goroutine在使用的变量,修改时需要序列化的读取。

    主要方式包括,通过channel的方式、sync/atomic等原子同步操作等

    如果你想读完以下内容,以便理解你的程序内在运行机制,说明你很聪明。

    但是不建议你这么聪明~

    1. 历史经验

    只有一个goroutine的时候,读写操作都会如程序定义的顺序执行。这是因为,尽管编译器和中央处理器是可能会改变执行顺序,但并不会影响编程语言定义的goroutine中的行为逻辑。但也是因为可能改变执行顺序,同样的操作在不同的goroutine中观察到的执行顺序并不一致。比如A goroutine执行了a=1;b=2;另一个goroutine观察到的结果可能是b先于a被赋值了

    为具体说明读写操作的要求,我们定义了之前某个版本中Golang程序中内存操作的一部分逻辑如下。如果事件e1发生在事件e2之前,我们说e2发生在e1之后.如果e1并不发生在e2之前,也不发生e2在之后,我们说e1和e2是同步发生的。

    在一个单goroutine的程序中,事件发生的顺序就是程序描述的顺序。

    满足如下两个条件的情况下,对变量v的读取操作r,是能够观察到对v的写入操作w的:

    1. r并不发生在w之前;
    2. r之前,w之后,再没有其他对v的写操作;

    为了保证对变量v对读取操作r,能够观察到特定的对v得写操作w,需要保证w是r唯一能够观察到写操作。因此,要保证r能够观察到w需要满足如下两个条件:

    1. w发生在r之前;
    2. 其他对v的写操作,要么发生在w之前,要么发生在r之后;

    这对条件是比第一个条件更加严格,它要求r和w的同时,没有其它的写操作(即和r或w同步的写操作);

    在单一goroutine里面,没有同步操作,所以以上两组条件是等价的。但是对于多goroutine,需要通过同步事件来确定顺序发生,从而保证读操作能够观察到写操作。

    在内存模型里面,变量v初始化为零值,也是一种写操作。

    读写大于单一机器码的变量的动作,实际操作顺序不定,

    1. 同步
      • 初始化

    程序初始化在单一goroutine里面,但是goroutine会创建其他goroutine,然后多个goroutine同步执行。

    如果package p引用了package q,q的init函数的执行,会先于所有p的函数执行。

    Main.main函数的执行,在所有init函数执行完后。

      • Goroutine的创建

    go表达式,会创建一个goroutine,然后该goroutine才能开始执行。

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

    以上代码示例,调用hello函数,可能在hello已经return到时候,f才回执行print。

      • Goroutine的销毁

    goroutine的退出时机并没有保证一定会在某个事件之前。

    var a string

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

    比如以上示例,对a的赋值,并不保证与hello本身的任何动作保持同步关系,所以也不能保证被其他任何goroutine的读操作观察到。事实上,任何一个激进的编译器都会把这里整个go表达式直接删掉,不做编译。

    如果一个goroutine的影响想被其他的goroutine观察到,必须通过同步机制(比如锁、channel)来确定相对顺序关系。

      • Channel通信

    channel通信是goroutines之间主要的同步方式。一般来说channel上的每一次send都会相应有另一个goroutine从此channel受到消息。

    同一个channel上,send操作总是先于相应的receive操作完成。

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

    以上示例,能够保证print出『hello, world』。对a的写,是先于往c中发送0,而从c中接收值,先于print。

    channel的关闭,先于接收到该channel关闭而发出来的0.

    在上面这个例子中用close(c)代替 c<-0,其效果是一样的。

    对于没有缓存的channel,receive发生在send完成之前。

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

    以上示例,依旧能够保证print出『hello, world』。对a的写,先于从c接收;从c接收,先于 c <- 0执行完 c <- 0执行完,先于print执行。

    但如果channel是缓存的(例如c = make(chan int, 1)),那么以上程序不能保证print出『hello, world』,甚至有可能出现空值、crash等情况;

    对于缓存容量为C的channel,第k次接收,先于K+C次发送完成

    这条规则概括了缓存和非缓存channel的规则。因此基于带缓存的channel,可以实现令牌策略:在channel中缓存的数量代表active的数量;channel的缓存容量表示最大可以使用的数量;发送消息表示申请了一个令牌,接收消息表示释放了一块令牌。这是限制并发常用的一种手段。

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

    以上示例程序,对于work list中的每一条,都创建了一个goroutine,但是用limit这个带缓存的channel来限制了,最多同时只能有3个goroutines来执行work方法

      • 锁机制

    sync包中实现了两个锁的数据类型,分别是sync.Mutex和sync.RWMutex

    对于任何的sync.Mutex和sync.RWMutex类型变量l,和n<m,对于l.Unlock()的调用n,总是先于对于l.Lock()的调用m

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

    如上示例能够保证print出『hello, world』。f中第一个l.Unlock()的调用,先于main中第二个l.Lock()的调用;第二个l.Lock()的调用先于print的调用;

    任何对于l.Rlock的调用(其中l为sync.RWMutex类型变量),总是有一个n,l.Lock在调用n执行l.Unlock之后才能return;对应的,l.RUnlock的执行在调用n+1执行l.Unlock之前

      • 单例(Once

    Sync包提供了一种安全的多goroutine种初始化机制,那就是Once类型。对于特定的方法f,多个线程都能调用Once.Do(f),但是只有一个线程会执行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()
    }

    这里print两次『hello, world』,但只有第一次调用doprint会执行setup赋值。

    1. 不正确的同步

    对于同步发生的读操作r和写操作w,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.这个事实颠覆了我们的一些习惯认知

    对于同步问题加锁一定要double check

    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()
    }

    如这个例子,不能保证观察到done的写操作时候,也能观察到对a的写操作。其中一个goroutine可能打印出空字符串。

    另外一种错误的典型如下:

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

    同上一个例子一样,这里对done得写观察,不能保证对a的写观察,所以也可能打印出空字符串。

    更甚,由于main和setup两个线程间没有同步事件,并不能保证main中一定能观察到done的写操作,因此main中的一直循环下去没有结束。(这里不是很理解,只能说setup的执行时机和main中for循环没有明确的相对先后和相对距离,所以可能导致循环很久setup还没执行,或执行了但是没有更新到main所读取的done

    还有以上风格的一些变体,如下:

    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的赋值而退出循环,但是也不能保证观察到g.msg的初始化值

    对于以上所有例子,解决方案是一样的,定义明确的同步机制。

  • 相关阅读:
    codeforces 1349 A 思维
    codeforces 1358 D 尺区
    codeforces 1251D 二分+贪心
    codeforces 1260 D 二分
    codeforces 1167B 交互ez
    volatile
    计算多级集合/树/部门树的深度
    Java学习路线-知乎
    day06
    day01_虚拟机与主机之间ip配置
  • 原文地址:https://www.cnblogs.com/icxy/p/9821580.html
Copyright © 2020-2023  润新知