08Go内存模型Memory module

Memory module

https://golang.org/ref/mem

如何保证在一个 goroutine 中看到在另一个 goroutine 修改的变量的值,如果程序中修改数据时有其他 goroutine 同时读取,那么必须将读取串行化。为了串行化访问,请使用 channel 或其他同步原语,例如 sync 和 sync/atomic 来保护数据。

Happen-Before

在一个 goroutine 中,读和写一定是按照程序中的顺序执行的。即编译器和处理器只有在不会改变这个 goroutine 的行为时才可能修改读和写的执行顺序。由于重排,不同的 goroutine 可能会看到不同的执行顺序。例如,一个goroutine 执行 a = 1;b = 2;,另一个 goroutine 可能看到 b 在 a 之前更新。

为了说明读和写的必要条件,我们定义了先行发生(Happens Before)。如果事件 e1 发生在 e2 前,我们也可以说 e2 发生在 e1 后。如果 e1不发生在 e2 前也不发生在 e2 后,我们就说 e1 和 e2 是并发的。

在单一的独立的 goroutine 中先行发生的顺序即是程序中code表达的顺序。

但是请注意 code 的顺序是保证单个 goroutine 里面的语义是正确的情况下,有可能指令还会重排,也就是看到的顺序未必是真正代码的执行顺序。只会对执行过程可能有调整,但是语义是没有影响的。

当下面条件满足时,对变量 v 的读操作 r 是被允许看到对 v 的写操作 w 的(一个读操作要看到前面的写操作):

  1. r 不先行发生于 w(先要执行w再执行r)。
  2. 在 w 后 r 前没有任何对 v 的其他写操作,那么看到的值就是刚刚写的值。

为了保证对变量 v 的读操作 r 看到对 v 的写操作 w,要确保 w 是 r 允许看到的唯一写操作。即当下面条件满足时,r 被保证看到 w:

  1. w 先行发生于 r
  2. 其他对共享变量 v 的写操作要么在 w 前,要么在 r 后(需要没有其他写操作与 w 或 r 并发发生)。

这一对条件比前面的条件更严格,需要没有其他写操作与 w 或 r 并发发生。

单个 goroutine 中没有并发,所以上面两个定义是相同的:

读操作 r 看到最近一次的写操作 w 写入 v 的值。

当多个 goroutine 访问共享变量 v 时,它们必须使用同步事件(互斥锁等)来建立先行发生这一条件,以此来保证读操作能看到需要的写操作。

  • 对变量 v 的零值初始化时在内存模型中表现的与写操作相同(初始化零值时等同于进行写操作)。
  • 对大于 single machine word 的变量的读写操作表现的像以不确定顺序对多个 single machine word 的变量的操作。

https://www.jianshu.com/p/5e44168f47a3

Memory Reordering

用户写下的代码,先要编译成汇编代码,也就是各种指令,包括读写内存的指令。CPU 的设计者们,为了榨干 CPU 的性能,无所不用其极,各种手段都用上了,你可能听过不少,像流水线、分支预测等等。其中,为了提高读写内存的效率,会对读写指令进行重新排列,这就是所谓的内存重排,英文为 MemoryReordering。

这一部分说的是 CPU 重排,其实还有编译器重排。比如:

X = 0
for i in range(100):
    X = 1
    print X

可能优化成如下:

X = 1
for i in range(100):
    print X

在单线程下以上两个部分代码执行没有什么区别。

但是如果这时有另外一个线程同时干了这么一件事:X = 0

在多核心场景下,没有办法轻易地判断两段程序是"等价"的。

现代 CPU 为了“抚平” 内核、内存、硬盘之间的速度差异,搞出了各种策略,例如三级缓存等。为了让 2(Thread2) 不必等待 1(Thread1) 的执行“效果”可见之后才能执行,我们可以把 1(Thread1) 的效果保存到 store buffer(cache line):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FWAcAAoN-1619529092888)(E:\MyFile\GO语言进阶训练营\第03周并行编程\CPu三级缓存.png)]

其中有两个线程 Thread1 与 Thread2。

对于单线程来说 store buffer是完美的,因为我们将A数据存到自己的 store buffer 之后打印的时候也是取自己的 store buffer 中取出来,没有语义歧义。

先执行 (1) 和 (3),将他们直接写入 store buffer,接着执行 (2) 和 (4)。“奇迹”要发生了:(2) 看了下 store buffer,并没有发现有 B 的值,于是从 Memory 读出了 0,(4) 同样从 Memory 读出了 0。最后,打印出了 00。因为我们现在存的值仅在 store buffer 中,并没有刷给我们的内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wLRTcZqV-1619529092889)(E:\MyFile\GO语言进阶训练营\第03周并行编程\缓存执行效果图.png)]

因此,对于多线程的程序,所有的 CPU 都会提供“锁”支持,称之为 barrier,或者 fence。它要求:barrier 指令要求所有对内存的操作都必须要“扩散”到 memory 之后才能继续执行其他对 memory 的操作。因此,我们可以用高级点的 atomic compare-and-swap,或者直接用更高级的锁,通常是标准库提供。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值