/

什么是并发语言的内存模型?

该篇文章首发于boyn.top,转载请声明

什么是内存模型

在并发语言中,共享变量的可见性问题一直是需要被十分小心地对待的,在不能保证修改是原子性的前提下,如果有大于1个线程(协程)同时修改一个变量的时候,其行为通常是未定义(即不知道会出现什么结果)的.所以,对于同样一段内存,我们需要使用一个语言特定的内存模型来对变量进行管理.

内存模型,其定义是:(这段话引用自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语言的内存模型定义了一种条件,在这种条件下,当一个协程读取一个变量时,保证这个协程能够看到不同的协程对这个变量作出的修改

在这篇文章中,我们将会对比Java和Go语言的内存模型他们之间的异同.为什么是这两个语言呢?首先,忽略掉语法上面的差异,Java和Go都是应用十分广泛的并发编程语言,他们也都有虚拟机和垃圾回收等机制,在运行时层面上,这两个语言是有许多不同的地方的,

当然,Go的并发是基于用户态的协程,而Java的并发依赖操作系统的线程调度,在这个方面我们也会比较他们的不同.

image-20200327173056097

happens-before原则

从上面的一段中,我们可以看得到,其实内存模型主要解决的问题就是多线程之间的可见性问题.而各位神仙们为了说明清楚,创造出来了happens-before原则 ,这个原则是对内存模型的高度抽象.在我们详细了解happens-before原则之前,首先要记住,我们说的A happens before B 并不指的是A操作在B之前发生,而是A的操作结果对B可见,这两者是有本质区别的!

无论是在Java,还是在Go中,都有十分显然的happens-before关系,这些关系我们无需指定任何加锁或者同步操作即可实现

通过了解以下的这些happens-before原则,可以让我们更少地写锁以及避免乱写锁的情况

  1. 在单个线程中,按照控制流顺序,前面的操作happens before后面的操作
  2. 同一个锁的情况下,unlock操作happens before后面的对锁的lock操作
  3. 线程(协程)的创建操作happens before 该线程(协程)的所有执行语句
  4. 线程(协程)的所有操作 happens before 该线程(协程)的终止操作
  5. 传递性:如果 A happens before B, B happens before C,那么 A happens before C

为什么会有这么复杂的规则?

看完了上面的一大段文字,我相信初次接触的人肯定会有疑问:为什么需要搞这些复杂的规则.这是由计算机的存储结构决定的.我们知道,计算机为了提升速度以及兼顾容量,将存储分为了若干层.这张图接触过计算机组成原理的人一定不会陌生.

image-20200327180707988

在程序运行时,我们的数据会分布在从主存到寄存器中,并且每个线程(协程)会在自己运行时,将变量读入到自己的工作内存当中,从而使得变量能够被更快地读取且修改,但是这就带来了一个问题,一个线程修改了变量之后,怎么让别的线程可以看到这个修改呢?特别在这个变量是共享变量的情况下,要如何保证呢?这就是我们上面所说的可见性问题,这个问题出现的原因被成为缓存局部性

何为缓存局部性

缓存局部性一般来说分为两种,时间局部性空间局部性.时间局部性指的是之前被访问到的数据,很有可能会被再次访问,空间局部性指的是访问过的数据,其在内存中附近的数据有可能会被访问.正是基于上面的理论,线程会将数据读入自己的工作内存中,工作内存一般来说在L1或者L2,能够更加高速地读取数据

image-20200327205929397

如何保证可见性

在很多时候,其实变量是不会产生竟态条件的,也就是说大部分时候的变量都只会被单个线程修改和读取,像这种变量保存在工作内存中往往可以得到更快的读取速度和更高的修改效率.但是有部分的变量还是需要被共享的,那么我们就需要来保证他们在多线程中的可见性.

在实际程序中,有许多方法可以保证他们的可见性,比如最简单的加锁,Java中也可以选择为变量加上volatile关键字,而在Go中,他们的处理方式是使用通道来共享内存,可以看一下这篇文章Do not communicate by sharing memory; instead, share memory by communicating.

而实际上在CPU中,使用的是伪共享机制,通过硬件,来标记复制Cache的无效性,当读取到这个Cache时,就会因为标记为读到了脏页,从而向主内存中请求最新版本的变量,通过这样的方式来达到可见性.

参考文章

关于图片和转载

知识共享许可协议
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制,你可以在 技术文章配图指南 一文中找到画图的方法和素材。