GO内存模型(同步机制)

@[TOC]

概念#

1. 先行发生#

The happens before relation is defined as the transitive closure of the union of the sequenced before and synchronized before relations.

翻译:
happens before 关系被定义为序列化 before 关系和同步化 before 关系的联合的传递闭包。
解析:

  1. happens before 关系描述两个事件之间的先后顺序
  2. 序列化 before 关系表示单个 goroutine 内事件间的顺序
  3. 同步化 before 关系表示不同 goroutine 间由同步原语同步的顺序
  4. happens before 关系是上述两个关系的联合
  5. 传递闭包表示如果 A happens before B,B happens before C, 那么 A happens before C

可以理解为程序的执行顺序,在单个协程当中,程序先行发生的顺序就是程序表达的顺序
举个例子:

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

我们说 a = "hello, world",先行发生于 print(a)

var a string

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

在多个协程对共享变量的读写中,为了保证读写的正确性,我们需要引入同步机制保证程序的顺序一致性执行。比如 channel,sync、atomic package。在上面的例子中 f () 有可能打印空字符串或者 "hello, world", 随机的。print(a) 不先行发生于 a赋值 ,a赋值也不先行发生于 print(a),我们就说这是并发的。我们应该如何保证 f2 的写入一定能被 f () 看到呢?这就是我们要讨论的内容。

编译器重排#

GO内存模型(同步机制)

GO内存模型(同步机制)

GO内存模型(同步机制)

同步机制#

以下是一些我们会用到的让程序保证顺序一致性执行的一些常用手段

init 函数#

一个函数的初始化函数可能在单个 goroutine 中执行,但是执行过程中有可能会开启另外一个协程并发执行,这时:
p 引入包 q,q 的 init 函数结束先行发生于 q 的所有 init 函数的开始
所有的 init 函数执行完了才会执行 main 函数

协程的创建#

新协程的创建先行发生与该协程的执行

var a string

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

这里 f () 一定能打印 "hello, world", 因为 a 的赋值先行发生于协程的创建,而协程的创建先行发生于函数的执行。

var a string

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

如果我们调整顺序

var a string

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

结果是随机的

PS E:\code\go\go_study\advanced\memery_model> go run memery.go
PS E:\code\go\go_study\advanced\memery_model> go run memery.go
hello, world
PS E:\code\go\go_study\advanced\memery_model> go run memery.go
hello, world
PS E:\code\go\go_study\advanced\memery_model> go run memery.go
hello, world
PS E:\code\go\go_study\advanced\memery_model> go run memery.go
hello, world
PS E:\code\go\go_study\advanced\memery_model> go run memery.go
hello, world
PS E:\code\go\go_study\advanced\memery_model> go run memery.go
PS E:\code\go\go_study\advanced\memery_model> go run memery.go
hello, world

也就是说 gorountine 的退出不会保证先行发生于程序的任何事件

channel#
  1. 无缓冲 channel
    当我们创建的 chan 不带缓冲,chan 的接收先行发生于 chan 的发送
var c = make(chan int)
var a string

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

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

<-c 先行发生于 c <- 0, 所以 a 一定能打印 "hello, world"

  1. 带缓冲 chanel
    当我们创建的 chan 带缓冲,当缓冲未满的时候,chan 的发送先行发生于 chan 的接收。当缓冲满了,chan 的接收先行发生于 chan 的发送
var c = make(chan int, 10)
var a string

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

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

c <- 0 先行发生于 <-c, 所以 a 一定能打印 "hello, world"

容量为 c 的带缓冲 chan 的第 k 个接收,先行发生于第 c+k 个发送

var limit = make(chan int, 3)

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

这里当 w () 执行耗时任务,chan 队列满了,第四个任务的 w 的 <-lmit 先行发生于 limit <- 1, 这样就能保证我们队列中只有 3 个任务在同时执行。

sync 包#
1. sync.mutex#
var l sync.Mutex
var a string

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

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

For any sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock() is synchronized before call m of l.Lock() returns.

l 执行了 n 次 Unlock, n 次 Unlock 先行发生于 n+1 次 Lock 操作

2. sync.rwmutex#

对于同一个 sync.RWMutex 变量 l:

  1. l.RLock () 和 l.RUnlock () 形成配对的读锁定 / 读解锁操作。
  2. 第 n 次 l.RLock () 发生在第 n 次 l.RUnlock () 之前。
  3. 第 n 次 l.RUnlock () 先行发生于第 n+1 次 l.Lock ()。
  4. 读锁定和读解锁按照配对嵌套的顺序执行。
  5. 每次读解锁先行发生于下一次写锁定。
3. sync.once#
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()
}

once 可以实现多个协程只执行一次 setup 代码,并且其他协程会阻塞直到 setup 函数返回才会继续运行下面的代码。

atomic#
go
var counter int64

func worker() {
  for {
    // 原子递增计数器
    atomic.AddInt64(&counter, 1) 
  }
}

func main() {
  for i := 0; i < 10; i++ {
    go worker()
  }

  time.Sleep(time.Second)

  // 原子读取计数器
  c := atomic.LoadInt64(&counter)
  fmt.Println(c)
}

相当于每次执行前都对变量加锁了。比如我们熟悉的 i++, 汇编成代码其实是 3 条指令,他是有可能被程序中断的,而 atomic 可以实现原子性操作。

参考文献#

go.dev/ref/mem
www.jianshu.com/u/89aa91463068

本作品采用《CC 协议》,转载必须注明作者和本文链接