volatile底层-CPU缓存一致性协议MESI

news2024/12/22 23:34:49

目录

volatile底层-CPU缓存一致性协议MESI

CPU高速缓存(Cache Memory)

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

目前流行的多级缓存结构

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

MESI协议缓存状态

MESI状态转换

多核缓存协同操作

​编辑单核读取

双核读取

修改数据

同步数据

​编辑volatile保证可见性 - 底层缓存一致性协议MESI[重点]

提出问题:

接下来进行揭晓:

volatile保证不了原子性 - 底层分析[重点]

为什么volatile保证不了原子性???

分析:

缓存行伪共享

MESI优化和他们引入的问题

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

Store Bufferes

Store Bufferes的两个风险

硬件内存模型


 volatile底层-CPU缓存一致性协议MESI

CPU高速缓存(Cache Memory)

CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。

在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。

时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

比如循环、递归、方法的反复调用等。

空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

比如顺序执行的代码、连续创建的两个对象、数组等。

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

https://www.processon.com/diagraming/6400394f5741de2e05c10a71

1.程序以及数据被加载到主内存中

2.指令和数据被加载到CPU的高速缓存

3.CPU执行指令,把结果写到高速缓存

4.高速缓存中的数据写回到主内存

目前流行的多级缓存结构

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

多级缓存结构

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

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

MESI协议缓存状态

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

缓存行:缓存数据存储的单元,一个缓存行为64byte(字节)

M:Modified 修改  E:Exclusive 独享,互斥

S: Sharede 共享    I:Invalid 无效

状态

描述

监听任务

M 修改 (Modified)

该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。

缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。

E 独享、互斥 (Exclusive)

该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。

缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。

S 共享 (Shared)

该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。

缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。

I 无效 (Invalid)

该Cache line无效。

举个例子:来说明MESI

线程1和线程2开启,会对x变量进行操作。

线程1先进行操作x时,为x&E。线程2之后也操作访问x,则线程1状态发生变化,为x&E->S,线程2为:x&S

线程2对于x进行修改值的操作,则线程1状态又发生变化,为x&E->S->I。线程2状态也发生变化,为x&S->M

注释:M表示修改,E表示独享,S表示共享,I表示无效

注意:

对于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状态(共享)。

volatile保证可见性 - 底层缓存一致性协议MESI[重点]

提出问题:

在一个CPU多核多线程的情况下,对于一个变量x加上了关键字volatile后,底层具体干了什么来实现volatile的可见性的?

private static volatile int x ;


public static void test01() {

    new Thread(()->{
        x=2;
    },"thread-a").start();

    new Thread(()->{
        x=3;
    },"thread-b").start();

}

先看看加上volatile关键字后代码进行转化到汇编指令后的形式:

接下来进行揭晓:

(1) 最初的方案:总线锁。

对于一个x加了volatile关键字后,保证了可见性。底层最初的方案,我们是在bus总线处加上了一个总线锁,一个CPU多核(多核说明有多个线程)可以对同一个x进行操作,但加上一个总线锁后,把通往内存的通道给锁死了。一个线程进行操作内存数据时,其它线程就不可进行操作啦。虽然保证了变量x的可见性,但是这种总线锁的模式把多核多线程的性能给完全丧失啦,所以之后引出了缓存一致性MESI优化理论

(2) 最终优化的方案,也是目前的方案:MESI缓存一致性协议

MESI优化理论说明:

 如图所示:ProcessOn FlowchartProcessOn Flowchart

从(1)中我们可以看出总线锁这种方式的弊端,所以使用MESI理论进行优化它。

MESI理论的情况比较复杂:

情况1:【在存储的变量x是小于缓存行(64字节)并且两个线程非同时修改变量x的】

对于多核多线程对变量x进行修改,我们把变量x的值写到本地缓存行中,本地缓存行的大小为64byte。不同核对应不同的线程,不同线程对应不同的缓存行来存储变量x。

由图可知:

thread a首先占用到了x,由于为独享,所以标记为x=2&E。

但是很快,线程thread b也会占用到x,所以此时x为共享,线程thread a标记x=2&E->S,thread b标记x=0&S。

之后thread b在thread a之前进行修改了x的值,此时会发出一个通知告诉thread a:"x已经被其它线程修改啦",那么此时thread a对应的x变量的空间就应该丢弃啦。所以此时thread a标记为x=2&E->S->I,thread a标记为x=3&S->M。

最后thread a会重新进行分配x,即:x=0->3

注释补充:其实在修改x变量值的时候,有一种假设,给缓存行进行上一个缓存行lock锁的,但是对于不同线程的缓存lock对象肯定是不同的,因为不同线程的缓存行是不共享的。那么上lock就没有任何的意义,所以这里使用到一个"通知"来保证多线程环境下,线程b修改了变量x,其它线程对此及时可见,赶紧丢弃的效果。

情况2:如果两个线程同时进行修改变量x的话

多核多线程的情况下,由于CPU的运行效率是非常高速的,所以同时进行修改变量x是有很大概率的。因此引出了"总线裁决"。总线裁决是一瞬间的裁决,根据CPU底层机制进行选定到底让哪一个线程的修改结果生效的决定。反正只会有一个线程的修改生效,然后会通知其它线程进行丢弃该修改的变量。

情况3:如果变量的大小大于缓存行的空间大小时

只说一句话,缓存一致性协议MESI升级为之前的总线锁(已解释过)。因为使用多个缓存行去存储同一个变量,无法保证操作的原子性。

volatile保证不了原子性 - 底层分析[重点]

private static volatile int counter ;

//伪代码
public static void test01() {

    new Thread(()->{
        counter++;//底层分为两步:1.counter=0 2.counter=counter+1
    },"thread-a").start();

    new Thread(()->{
        counter++;
    },"thread-b").start();

}

为什么volatile保证不了原子性???

通过前面的分析可知,volatile是可以保证可见性的,但是保证不了原子性。

分析:

如图所示:ProcessOn Flowchart

 counter++的底层分为两步:1.counter=0 2.counter=counter+1。

(1) thread-a线程从缓存行中读取这两步对应的指令到寄存器中进行执行,执行完毕后把counter=1的结果写回到缓存行中。根据前面的MESI缓存一致性原则可知,此时我们会给还没有执行完的thread-b线程发出一个"通知",告诉thread-b:你的counter失效啦!!现在counter=1

(2) 但是此时thread-b对于自己执行的counter=counter+1这个指令操作已经读取到寄存器中正在执行了,只有缓存行中的counter=0指令才会失效,此时缓存行的counter=1。但是寄存器中的counter=counter+1指令会正常执行完毕(读取到寄存器时的counter=0,所以最终结果为counter=1)。最终结果counter=1会覆盖thread-b后的缓存行的counter=1,故最终thread-b的结果为:counter=1。

(本以为thread-a通知thread-b:counter变为了1后,最终结果会为2,但是为1,符合可见性,但是不符合原子性)。

(3) 最终thread-a得出counter++后得出的结果为counter=1,thread-b得出的结果也为counter=1,两个线程一块写回到内存中去。会覆盖的写回,最终结果就是counter=1

(4) 这个时候出现问题:明明执行了两次counter++,为什么最终counter=1????

总结:出现这种情况,就是因为volatile保证不了多条指令的原子性。

缓存行伪共享

什么是伪共享?

CPU缓存系统中是以缓存行为单位进行存储的。目前主流的CPU Cache 的 Cache Line 大小都是64Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。

举个例子:

现在有2个long 型变量 a 、b,如果有t1在访问a,t2在访问b,而a与b刚好在同一个cache line中,此时t1先修改a,将导致b被刷新

怎么解决伪共享?

Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置 -XX:-RestrictContended 才会生效。

@sun.misc.Contended
public final static class TulingVolatileLong {
    public volatile long value = 0L;
    //public long p1, p2, p3, p4, p5, p6;
}

MESI优化和他们引入的问题

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

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

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

Store Bufferes

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

但是使用Store Bufferes有两个风险。

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。

注: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;
}

加入Store Buffer和Invalidate Queue后的整体图:

ProcessOn Flowchart

 

参考资料:《并发编程的艺术》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/386770.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Modelsim 操作结构和流程

用到的命令一般都写到.do文件中,使用脚本语言进行批量处理。Step 1: Map librariesStep 2: Compile the designStep 3: Optimize the design (OPTIONAL)Step 4: Load the design into the simulatorStep 5: Run the simulationStep 6: Debug the design Note: Desig…

10、DNS部署和安全(图文解析)~千锋

目录 概述 域名组成 监听端口 客户机的域名解析过程 DNS解析分类 DNS服务器搭建 概述 Domain Name Service 域名服务 作用:为客户机提供域名解释服务器 域名组成 域名组成概述 “www.sina.com.cn”是一个域名,从严格意义上讲”sina.com.cn”才…

CV学习笔记-Faster-RCNN

Faster R-CNN 文章目录Faster R-CNN1. 目标检测算法1.1 计算机视觉有五大应用1.2 目标检测任务1.3 目标检测算法概述2. 边框回归(Bounding-Box regression)2.1 IoU2.2 统计学中的指标2.3 边框回归3. Faster-RCNN网络3.1 Conv layers3.2 Region Proposal …

Python每日一练(20230304)

目录 1. 移除链表元素 ★ 2. K 个一组翻转链表 ★★★ 3. 三角形最小路径和 ★★ 1. 移除链表元素 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val val 的节点,并返回 新的头节点 。 示例 1: 输入&…

曹云金回归、于谦电影杀青,德云社想不火都难

说起民间最大的相声社团,首屈一指的要属德云社,之所以说德云社最大,主要是优秀相声演员够多。德云社在郭德纲的带领下,如今已经是人才济济,听说最近队伍会进一步壮大,前徒弟曹云金也要回归了。 当年曹云金作…

c语言期末突击讲义+笔记

【祝逢考必过&#xff0c;心想事成~~~~】 【一定能过&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;】 C 语言第一课 一、 固定格式 例 1&#xff1a; #include<stdio.h> int main() { return 0; }### 二、 printf 语句 例 1&#xff1a; #inclu…

springsession中redis键值过期后,后置处理

Redis键空间通知&#xff08;说明&#xff09; 基于redis的发布订阅功能&#xff0c;实现&#xff0c;当对一个key做出操作后&#xff0c;可以对改建进行监听&#xff0c;来做后续的处理。 在spring-session中支持当一个session过期后&#xff0c;来做一些后续的操作。那是如何…

Spring-注解注入

一、回顾XML注解 bean 配置 创建 bean public class Student { } 配置 xml bean <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/beans"xmlns:xsi"http://www.w3.org/2001/XMLSche…

2023年人工智能技术趋势

随着DALL-E 2、ChatGPT等越来越多应用的出现&#xff0c;人们越来越期待人工智能在2023年给人们带来的突破性改进。2023年人工智能技术趋势人工智能(AI)的应用越来越具有创新性&#xff0c;广泛的应用程序使用 AI 从头开始创建令人印象深刻的文本、视频和图像。越来越多的新应用…

【深度学习】扩散模型(Diffusion Model)详解

【深度学习】扩散模型&#xff08;Diffusion Model&#xff09;详解 文章目录【深度学习】扩散模型&#xff08;Diffusion Model&#xff09;详解1. 介绍2. 具体方法2.1 扩散过程2.2 逆扩散过程2.3 损失函数3. 总结4. 参考1. 介绍 扩散模型有两个过程&#xff1a; 扩散过程&…

蓝桥杯2012年省赛棋盘放麦子

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 你一定听说过这个故事。国王对发明国际象棋的大臣很佩服&#xff0c;问他要什么报酬&#xff0c;大臣说&#xff1a;请在第 11 个棋盘格放 11 粒麦子&#xff0c;在…

【宝藏博客系列】用C语言中的普通数组构建一棵特殊的完全二叉树----堆(内容超劲爆的喔~)

目录 0.前言 1. 用数组表示存储一棵完全二叉树 2. 数组表示的完全二叉树的性质 3. 堆的基本概念 3.1 堆的核心性质 3.2 堆顶的性质 3.3 堆的单支性质 3.4 堆的左右支关系 4. 用代码实现堆 4.1 堆类的实现 4.2 堆的初始化 4.3 堆的销毁 4.4 获取堆顶的数据 4.5 …

惠普电脑怎么录屏?图文教学,3种简单易懂的录屏方法

如果你需要在惠普电脑上录制视频或游戏&#xff0c;那么录屏软件是必不可少的工具。但是&#xff0c;有很多人不知道惠普电脑怎么录屏&#xff0c;或者不知道该选择哪种录屏软件进行录屏。今天小编将向小伙伴介绍3种简单易懂的惠普电脑录屏方法&#xff0c;帮助你轻松录制高质量…

信息系统基本知识(三)软件工程

1.4 软件工程 定义&#xff1a;将系统的、规范的、可度量的工程化方法应用于软件开发、运行和维护的全过程即上述方法的研究 软件工程由方法、工具和过程三个部分组成 1.4.1 需求分析 软件需求是指用户对新系统在功能、行为、性能、设计约束等方面的期望。 需求层次 业务…

FFmpeg入门 - Android移植

1、FFmpeg编译与集成FFmpeg的安卓交叉编译网上有很多的资料,基本上都是些编译配置而已。可以直接将我的脚本放到ffmpeg源码根目录,修改下NDK的路径和想要编译的ABI之后直接执行。然后就能在android目录里面得到编译好的so和.h如果的确编译出现问题,也可以直接用我编出来的库。将…

Enlight Software Ltd.官方正式推出〔金融帝国实验室试玩版本〕(Capitalism Lab Demo Version)

Enlight Software Ltd.官方正式推出〔金融帝国实验室试玩版本〕&#xff08;Capitalism Lab Demo Version&#xff09; ————————————— 与一般游戏的试玩版本不同&#xff0c;〔金融帝国实验室试玩版本〕是一款功能齐全的游戏&#xff0c;包含完整版本的所有功能…

spark sql(三)逻辑计划解析(analyzer)

1、前言 Spark SQL 逻辑计划在实现层面被定义为 LogicalPlan 类 。 从 SQL 语句经过 SparkSqlParser解析生成 Unresolved LogicalPlan &#xff0c;到最终优化成为 Optimized LogicalPlan &#xff0c;这个流程主要经过3 个阶段。 这 3 个阶段分别产生 Unresolved LogicalPlan,…

组合模式-文件目录管理

什么是组合模式&#xff1f; 组合模式是一种结构型设计模式&#xff0c;它允许你将对象组合成树形结构来表现"部分-整体"的层次结构。组合模式使得客户端可以统一地对待单个对象和对象组合&#xff0c;从而使得系统更加灵活。 在组合模式中&#xff0c;有两种基本类…

SQL开发管理工具,SQL Studio成数据库管理工具热门

达梦数据库冲击IPO成功&#xff1b;麒麟软件等国产Linux桌面操作系统在国防、教育等行业达到百万规模级应用&#xff1b;阿里云计算操作系统取得重大突破&#xff0c;阿里云市场份额全球第三&#xff1b;宝兰德、中创等厂商的应用服务器中间件能够实现对IBM和Oracle相关产品的替…

Linux学习第十一节-磁盘类型介绍和挂载

1.磁盘类型介绍 IDE接口类型&#xff1a;主要用于个人家用计算机领域&#xff0c;优点价格便宜&#xff0c;缺点数据传输速度慢&#xff1b; SCSI接口类型&#xff1a; 主要用于服务器领域&#xff0c;数据传输速度快&#xff0c;支持热插拔&#xff1b; SATA接口类型&…