并发与高并发

二、CPU多级缓存

2-1 CPU高级缓存-缓存一致性(MESI协议)

1.为什么需要CPU缓存?

答:CPU的频率太快了,快到主存赶不上,这样在处理器时钟周期内,CPU常常要等待主存,浪费了资源。所以缓存的出现,是为了缓解CPU和内存之间速度不匹配的问题。(结构:cpu->cache->memory)

2.CPU缓存有什么意义?

答: 1)   时间局部性:如果某个数据被访问,那么在不久的将来它有可能被再次访问。

   2)空间局部性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问。 

3.缓存一致性(MESI):用于保证多个CPU cache之间缓存共享数据的一致。

  M:Modifed(被修改) :该缓存行只被缓存在该CPU的缓存中,并且是被修改过的,因此,他与主存中的数据是不一致的,该缓存行中的内存需要在未来的某个时间点写回主存,这个时间点我们是允许其他CPU读取主存中相应的内存之前,当这里面的值被写回主存之后,该缓存行的状态会变成E这个状态,也就是“独享”。

  E:Exclusive(独享):它的缓存行只被缓存在该CPU的缓存中,它是未被修改过的,是与主存中的数据一致的,这个状态是在任何时刻,当有其他CPU读取该内存时,变成S,也就是变成“共享”状态。

  S:shared(共享) :它意味着该缓存行可能被多个CPU进行缓存,并且各缓存中的数据与主存数据是一致的,当有一个CPU修改该缓存行的时候,其他CPU中该缓存行是可以被作废的,变成I,也就是“无效的”状态。

  I:Invalid(无效的):代表这个缓存是无效的,可能是有其他CPU修改了该缓存行。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  local read:读本地缓存中的数据。

  local write:将数据写到本地的缓存里面。

  remote read:将内存中的数据读取过来。

  remote write:将数据写回到主存里面去。

         (状态转换关系图)

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

补充:

带有高速缓存的CPU执行计算的流程

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU的高速缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

目前流行的多级缓存结构

由于CPU的运算速度超越了1级缓存的数据I\O能力,CPU厂商又引入了多级的缓存结构。

多级缓存结构

多核CPU多级缓存一致性协议MESI

多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。

MESI协议缓存状态

MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:

缓存行(Cache line):缓存存储数据的单元。

状态 描述 监听任务
M 修改 (Modified) 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。
E 独享、互斥 (Exclusive) 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S 共享 (Shared) 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I 无效 (Invalid) 该Cache line无效。

注意:
对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。

MESI状态转换

理解该图的前置说明:
1.触发事件

触发事件 描述
本地读取(Local read) 本地cache读取本地cache数据
本地写入(Local write) 本地cache写入本地cache数据
远端读取(Remote read) 其他cache读取本地cache数据
远端写入(Remote write) 其他cache写入本地cache数据

2.cache分类:
前提:所有的cache共同缓存了主内存中的某一条数据。

本地cache:指当前cpu的cache。
触发cache:触发读写事件的cache。
其他cache:指既除了以上两种之外的cache。
注意:本地的事件触发 本地cache和触发cache为相同。

上图的切换解释:

状态 触发本地读取 触发本地写入 触发远端读取 触发远端写入
M状态(修改) 本地cache:M
触发cache:M
其他cache:I
本地cache:M
触发cache:M
其他cache:I
本地cache:M→E→S
触发cache:I→S
其他cache:I→S
同步主内存后修改为E独享,同步触发、其他cache后本地、触发、其他cache修改为S共享
本地cache:M→E→S→I
触发cache:I→S→E→M
其他cache:I→S→I
同步和读取一样,同步完成后触发cache改为M,本地、其他cache改为I
E状态(独享) 本地cache:E
触发cache:E
其他cache:I
本地cache:E→M
触发cache:E→M
其他cache:I
本地cache变更为M,其他cache状态应当是I(无效)
本地cache:E→S
触发cache:I→S
其他cache:I→S
当其他cache要读取该数据时,其他、触发、本地cache都被设置为S(共享)
本地cache:E→S→I
触发cache:I→S→E→M
其他cache:I→S→I
当触发cache修改本地cache独享数据时时,将本地、触发、其他cache修改为S共享.然后触发cache修改为独享,其他、本地cache修改为I(无效),触发cache再修改为M
S状态(共享) 本地cache:S
触发cache:S
其他cache:S
本地cache:S→E→M
触发cache:S→E→M
其他cache:S→I
当本地cache修改时,将本地cache修改为E,其他cache修改为I,然后再将本地cache为M状态
本地cache:S
触发cache:S
其他cache:S
本地cache:S→I
触发cache:S→E→M
其他cache:S→I
当触发cache要修改本地共享数据时,触发cache修改为E(独享),本地、其他cache修改为I(无效),触发cache再次修改为M(修改)
I状态(无效) 本地cache:I→S或者I→E
触发cache:I→S或者I →E
其他cache:E、M、I→S、I
本地、触发cache将从I无效修改为S共享或者E独享,其他cache将从E、M、I 变为S或者I
本地cache:I→S→E→M
触发cache:I→S→E→M
其他cache:M、E、S→S→I
既然是本cache是I,其他cache操作与它无关 既然是本cache是I,其他cache操作与它无关

下图示意了,当一个cache line的调整的状态的时候,另外一个cache line 需要调整的状态。

  M E S I
M × × ×
E × × ×
S × ×
I

举个栗子来说:

假设cache 1 中有一个变量x = 0的cache line 处于S状态(共享)。
那么其他拥有x变量的cache 2、cache 3等x的cache line调整为S状态(共享)或者调整为 I 状态(无效)。

多核缓存协同操作

假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。

单核读取

那么执行流程是:
CPU A发出了一条指令,从主内存中读取x。
从主内存通过bus读取到缓存中(远端读取Remote read),这是该Cache line修改为E状态(独享).

双核读取

那么执行流程是:
CPU A发出了一条指令,从主内存中读取x。
CPU A从主内存通过bus读取到 cache a中并将该cache line 设置为E状态。
CPU B发出了一条指令,从主内存中读取x。
CPU B试图从主内存中读取x时,CPU A检测到了地址冲突。这时CPU A对相关数据做出响应。此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享)。

修改数据

那么执行流程是:
CPU A 计算完成后发指令需要修改x.
CPU A 将x设置为M状态(修改)并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为I状态(无效)
CPU A 对x进行赋值。

同步数据

那么执行流程是:

CPU B 发出了要读取x的指令。
CPU B 通知CPU A,CPU A将修改后的数据同步到主内存时cache a 修改为E(独享)
CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为S状态(共享)。

MESI优化和他们引入的问题

缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。

CPU切换状态阻塞解决-存储缓存(Store Bufferes)

比如你需要修改本地缓存中的一条信息,那么你必须将I(无效)状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。应为这个等待远远比一个指令的执行时间长的多。

Store Bufferes

为了避免这种CPU运算能力的浪费,Store Bufferes被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
这么做有两个风险

Store Bufferes的风险

第一、就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
第二、保存什么时候会完成,这个并没有任何保证。

value = 3;

void exeToCPUA(){
  value = 10;
  isFinsh = true;
}
void exeToCPUB(){
  if(isFinsh){
    //value一定等于10?!
    assert value == 10;
  }
}

试想一下开始执行时,CPU A保存着finished在E(独享)状态,而value并没有保存在它的缓存中。(例如,Invalid)。在这种情况下,value会比finished更迟地抛弃存储缓存。完全有可能CPU B读取finished的值为true,而value的值不等于10。

即isFinsh的赋值在value赋值之前。

这种在可识别的行为中发生的变化称为重排序(reordings)。注意,这不意味着你的指令的位置被恶意(或者好意)地更改。

它只是意味着其他的CPU会读到跟程序中写入的顺序不一样的结果。

顺便提一下NIO的设计和Store Bufferes的设计是非常相像的。

硬件内存模型

执行失效也不是一个简单的操作,它需要处理器去处理。另外,存储缓存(Store Buffers)并不是无穷大的,所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列。它们的约定如下:

  • 对于所有的收到的Invalidate请求,Invalidate Acknowlege消息必须立刻发送
  • Invalidate并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
  • 处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate。

即便是这样处理器已然不知道什么时候优化是允许的,而什么时候并不允许。
干脆处理器将这个任务丢给了写代码的人。这就是内存屏障(Memory Barriers)。

写屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb)是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。

读屏障Load Memory Barrier (a.k.a. LD, RMB, smp_rmb)是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。

void executedOnCpu0() {
    value = 10;
    //在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。
    storeMemoryBarrier();
    finished = true;
}
void executedOnCpu1() {
    while(!finished);
    //在读取之前将所有失效队列中关于该数据的指令执行完毕。
    loadMemoryBarrier();
    assert value == 10;
}

现在确实安全了。完美无暇!

后记

然而,对于程序员来说简直是一个灾难。不想和平台耦合我们要跨平台。Write One,Run Everywhere!
幸好java解决了这个问题,至于如何解决的请关注JMM(JavaMemoryMode)与物理内存相爱相杀。

原文地址:https://www.cnblogs.com/jmy520/p/11632665.html

时间: 2024-11-02 20:54:39

并发与高并发的相关文章

并发、高并发、集群的含义?

并发:多个用户同时向服务器发出请求,服务器会开启多个线程,每个线程服务一个用户 高并发:当用户量达到成千上万,会导致服务器资源不再充足,最终使服务器内存溢出 集群:可以解决高并发问题  集群将一个应用拷贝多份,搭在多台服务器(机器),多台服务器构成一个集群 原文地址:https://www.cnblogs.com/codeXi/p/11429871.html

【高并发】高并发环境下诡异的加锁问题(你加的锁未必安全)

声明 特此声明:文中有关支付宝账户的说明,只是用来举例,实际支付宝账户要比文中描述的复杂的多.也与文中描述的完全不同. 前言 很多网友留言说:在编写多线程并发程序时,我明明对共享资源加锁了啊?为什么还是出问题呢?问题到底出在哪里呢?其实,我想说的是:你的加锁姿势正确吗?你真的会使用锁吗?错误的加锁方式不但不能解决并发问题,而且还会带来各种诡异的Bug问题,有时难以复现! 在上一篇<[高并发]如何使用互斥锁解决多线程的原子性问题?这次终于明白了!>一文中,我们知道在并发编程中,不能使用多把锁保护

[高并发]Java高并发编程系列开山篇--线程实现

ava是最早开始有并发的语言之一,再过去传统多任务的模式下,人们发现很难解决一些更为复杂的问题,这个时候我们就有了并发. 引用 多线程比多任务更加有挑战.多线程是在同一个程序内部并行执行,因此会对相同的内存空间进行并发读写操作.这可能是在单线程程序中从来不会遇到的问题.其中的一些错误也未必会在单CPU机器上出现,因为两个线程从来不会得到真正的并行执行.然而,更现代的计算机伴随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行. 那么,要开始Java并发之路,就要开始

Java并发和高并发学习总结(四)- J.U.C之工具类

1.总览 CountDownLatch允许一个或多个线程等待某些操作完成 Semaphore Java版本的信号量实现 CyclicBarrier 一种辅助性的同步结构,允许多个线程等待到达某个屏障 Exchanger 在线程间交换数据的一种手段 2.CountDownLatch 当一个或多个线程需要等待其他线程完成操作时,就可以使用CountDownLatch了,当然,最简单的你也可以使用join方法 2.1.join public class JoinTest { public static

并发与高并发-并发模拟代码

一.CountDownLatch 1.背景: (1)countDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier.Semaphore.concurrentHashMap和BlockingQueue. (2)存在于java.util.cucurrent包下. 2.概念 (1)countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行. (2)是通过一个计数器来实现的,计数器的初始值是线程的数量.每当一个线程执行完毕后,计数器的值就-1

并发与高并发(十三)J.U.C之AQS

前言 什么是AQS,是AbstractQueuedSynchronizer类的简称.J.U.C大大提高了并发的性能,而AQS又是J.U.S的核心. 主体概要 J.U.C之AQS介绍 J.U.C之AQS-CountDownLatch J.U.C之AQS-Semaphore J.U.C之AQS-CyclicBarrier J.U.C之AQS-ReentrantLock与锁 主体内容 总结 原文地址:https://www.cnblogs.com/jmy520/p/12337891.html

并发与高并发(八)-线程安全性-原子性-synchronized

前言 闲暇时刻,谈一下曾经在多线程教程中接触的同步锁synchronized,相当于复习一遍吧. 主要介绍 synchronized:依赖JVM Lock:依赖特殊的CPU指令,代码实现,ReetrantLock 主体内容 一.那么我们主要先讲解一下关于同步锁synchronized的作用范围. 1.修饰代码块:作用范围-大括号括起来的代码,作用于调用这个代码块的对象,如果不同对象调用该代码块就不会同步. 2.修饰方法:作用范围-整个方法,作用于调用这个方法的对象. 3.修饰静态方法:作用范围-

并发与高并发-线程安全性-可见性

前言 乍看可见性,不明白它的意思.联想到线程,意思就是一个线程对主内存的修改及时的被另一个线程观察到,即为可见性. 那么既然有可见性,会不会存在不可见性呢? 答案是肯定的,导致线程不可见的原因是什么呢? 有三个原因: (1)线程交叉执行. (2)重排序结合线程交叉执行. (3)共享变量更新后的值没有在工作内存与主存间及时更新. 主体内容 一.这里的可见性涉及到synchronized,顺便了解一些一下JMM对synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存

并发与高并发(十四)J.U.C组件拓展

前言 J.U.C的拓展组件有哪些?分别在什么场景下使用?如何使用? 主体概要 J.U.C-FutureTask J.U.C-ForkJoin J.U.C-ForkJoin 主体内容 一.J.U.C-FutureTask 1.这里要介绍的第一个组件是FutureTask,这个组件是J.U.C里面的,但它不是AQS的子类.这个类对线程结果的处理值得我们去学习.在学习Java多线程的时候,一定接触过Thread,Runnable,一种是直接继承Thread,另一种就是实现Runnable,这两种方式有