CPU(Central Processing Unit)
CPU是一块超大规模的集成电路,是一台计算机的运算核心(Core)和控制核心( Control Unit)。它的功能主要是解释计算机指令、处理计算机软件中的数据。主要组成分为三个部分,由CPU内部总线连接起来:
- 算术逻辑运算单元(ALU,Arithmetic Logic Unit):负责执行所有的数学和逻辑工作
- 控制单元(CU,Control Unit):根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。
- 存储单元:包括CPU片内缓存和寄存器组,是CPU中暂时存放数据的地方。采用寄存器,可以减少CPU访问内存的次数,从而提高了CPU的工作速度。
寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据。而通用寄存器用途广泛并可由程序员规定其用途,通用寄存器的数目因微处理器而异。
同一个时刻单个CPU核上只能运行一个任务,即一个核线程(kernel thread)
单个CPU多个内核
多核处理器是指在一枚处理器中集成两个或多个完整的计算引擎(内核),核心是多个,但是其他硬件还都是多个核心所共同拥有,如L3缓存
一个CPU的每个核心拥有独立的运算处理单元、控制器、寄存器、L1、L2缓存
多个核心共享最后一层CPU缓存L3,是一个进程的多个线程可以共享信息的基础
单个核心多个线程
Intel开发出了超线程技术(HyperthreadingTechnology),可以在一个物理内核上模拟出两个逻辑内核,即平时常见的4核8线程。其本质原理是让两个核线程同时使用CPU中的不同部分(处理核心,寄存器,缓存)
因为逻辑内核是模拟出来的,因此就算是双核四线程还是只有2个一级缓存L1,2个二级缓存L2,一个三级缓存L3
多CPU
真正意义上的多CPU不光是处理器核心是两个,其他例如缓存等硬件配置也都是双份的。一个CPU对应一个物理插槽,多处理器间通过QPI总线相连。
串行,并发,并行
串行:多个线程,执行时一个执行完再执行另一个。
并发(concurrency):多个线程以短暂的时间片为单位交替执行。
并行(parallelism):每个线程分配给独立的核心,同时运行。
内存
内存是由一系列的存储单元组成的,每个存储单元存储固定大小的数据,且有一个唯一地址。
当需要读内存时,将地址信号放到地址总线上传给内存,内存解析信号并定位到存储单元,然后把该存储单元上的数据放到数据总线上,回传。写内存时,系统将要写入的数据和单元地址分别放到数据总线和地址总线上,内存读取两个总线的内容,做相应的写操作。
内存存取效率,跟次数有关。
构成内存的SDRAM(同步动态随机存储器)是一种断电即掉的非永久随机存储器。只含有一个晶体管和一个电容器,集成度非常高可用轻易的做到大容量,但因为靠电容器来存储信息所以需要不间断刷新电容器的电荷,而充放电之间的时间差导致DRAM的数据读写速度较SRAM慢的多。
OS内核启动以后, 内核将物理内存管理起来。内核提供虚拟内存管理机制给每个进程(应用程序App)内存服务。
进程(应用App)需要内存时,OS给其分配一个独立,相互不影响的虚拟内存空间。然后OS再从自己管理的物理内存里面分配出来物理内存页,然后通过页表或者段表,将分配的虚拟内存与物理内存映射起来,这样,读写虚拟内存地址最终通过映射来使用物理内存地址,
内存分配策略:可变大小分配策略,固定尺寸分配策略
内存分配的开销:维护分配和释放内存空间的大小(可变大小分配策略下)的开销,对齐开销
内存分配由谁负责
分层次,由内到外:
- 操作系统内核提供最基本服务
- 编译器的缺省运行库提供自己的分配服务,可能只是对操作系统的算法进行了封装,也可能有重载
- 标准运行环境(框架等)提供的内存分配服务
- 用户自定义的运行环境(框架等)和分配器提供的服务
每个进程会把虚拟内存空间分成4个段(代码段, 数据段,堆,栈)。
- 代码段:用来存放进程(应用App)的代码指令。
- 数据段:用来存放全局变量的内存。
- 堆:调用os的malloc/free 来动态分配的内存。
- 栈:用来存放局部变量,函数参数,函数调用与跳转。
1页表与物理内存映射关系
CPU缓存
CPU的运算处理速度与内存读写速度的差异非常巨大,为了解决这种差异,CPU缓存应运而生,它是介于CPU处理器和内存之间的临时数据交换的缓冲区。
物理原理
由SRAM(静态随机存储器)构成,相比起DRAM更复杂,占据空间大,成本高,集成度低,因为不需要刷新电路所以读写速度快。
逻辑原理
时间局部性原理:被引用过的内存位置很可能在不远的将来还会被多次引用。
空间局部性原理:如果一个内存位置被引用了,那么程序很可能会在不远的将来引用该内存位置附近的内存位置。
多个核心共享CPU缓存是多个线程可以共享信息的基础
缓存分为数据缓存(Data Cache,D-Cache)和一级指令缓存(Instruction Cache,I-Cache),分别用于存放数据和执行数据的指令解码,两者可以同时被CPU访问,减少了CPU多核心,多线程争用缓存造成的冲突。
从L0, L1, L2到磁盘,每一层存储的数据都是下一层的数据的子集
因为时间局限性原理,CPU缓存在不命中的时候,向下层缓存请求的时候,返回的数据是以一个缓存行为单位的。直到到达L1。L1将数据行放置到自己的缓存行之后,从被存储的缓存行中抽取出CPU真正需要的字w,然后将它返回给CPU。
CPU缓存读写
从CPU缓存读取时从上往下依次查找的,下层查找到包含字w的缓存行之后,再由下层将该缓存行返回给上一层高速缓存,上一层高速缓存将这个缓存行放在它自己的一个高速缓存行中之后,继续返回给上一层,直到到达L1。
写一个已经缓存了的字w(写命中):更新本级缓存的w副本之后,依次更新它的下一级缓存。有两个思路:
- 直写:即立即将包含w的高速缓存行写回到第一层的缓存层, 这样做简单,但是如果大家都不停的写势必会产生很大的总线流量,不利于其他数据的处理。
- 写回:尽可能的推迟更新,只有当需要使用这个更新过的缓存行时才把它写回到紧接着的第一层的缓存中,这样总量流量减少了,但是增加了复杂性,高速缓存行必须额外的维护一个“修改位”,表明这个高速缓存行是否被修改过。
写一个不在缓存中的字w时(写不命中):也有两种思路
- 写分配,就是把不命中的缓存先加载过来,然后再更新整个缓存行,后面就是写命中的处理逻辑了
- 非写分配:直接把这个字写到下一层。
保持多CPU缓存一致性的方法:
缓存一致性协议MESI
- 写回/懒更新:当一个CPU在修改它的缓存之前,会通过最后一级缓存L3(因为最后一级缓存是多核心共享的)或者总线(多CPU跨插槽的情况)广播到其他CPU的缓存,使其它存在该缓存数据的缓存行无效,然后再更改自己的缓存数据,并标记为M,当其他CPU缓存需要读取这个被修改过的缓存行时(或者由于冲突不命中需要被置换出去时),会导致立即将这个被修改过的缓存行写回到内存,然后再从内存加载最新的数据到自己的缓存行。
- 使用“直泻时”,更改马上写回内存,其他CPU通过嗅探技术,从总线上得知相关的缓存行数据失效,则立即使自己相应的缓存行无效,从而再下次读不命中的时候重新到内存加载最新的数据。
- 当CPU修改自己的缓存行数据时,主动将相关的更新通过最后一级缓存L3或者总线(如果是多CPU跨插槽的情况)发送给其它存在相关缓存的CPU,使它们同步的更新自己的缓存到一致。
操作系统内核
内核是操作系统的核心。是操作系统执行的第一道程序,被率先加载到内存中开始系统行为。内核始终保持在主内存中直到系统被关闭。
内核将用户输入的命令转换成计算机硬件能理解的机器语言将,是系统应用软件和硬件的桥梁。
内核的主要职责是:进程管理、磁盘管理、任务调度、内存管理等。
进程,线程,协程,任务调度器
进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统资源分配和独立运行的最小单位。
代码是静态的指令,而进程就是动态执行任务的过程。
每个进程都有自己的独立内存空间,通信和上下文进程间的切换(栈、寄存器、虚拟内存、文件句柄等)开销比较大,但相对比较稳定安全。
线程
线程是任务调度和系统执行的最小单位
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),
一个进程可以拥有多个线程,同属一个进程的线程共享进程所拥有的全部资源。
线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
单核环境下,多线程时间片轮转调度并发执行。
时间片轮转调度:CPU将时间分成小片,每个时间片轮流执行所有线程,看起来就好像所有线程同时在进行一样,这种情况下每个线程需要的运行时间比单个计算任务本身耗时要长。
多核环境下,多线程会被分发给不同的内核并行执行,如果内核数量不够则分片轮转调度并发执行。
严格讲应该是线程能够获得CPU资源,进程对CPU资源的获取也是体现在线程上的。至于CPU内核数,和进程线程没直接关系。操作系统(OS)可以把某个进程部署在某个CPU核上,当然这要取决于系统设计。
堆内存、代码区一般属于一个进程,但是栈却是属于一个线程的,且每个线程拥有一个独立的栈
协程
协程是一种用户态的轻量级线程,状态切换及上下文切换等资源调度完全由用户控制,不受内核调度
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销
协程可以不加锁的访问全局变量,所以上下文的切换非常快
计算密集型任务并发时需要思考的问题
- 这样的任务CPU使用率高,为了防止CPU爆满其实并不应该开特别多的线程,可以通过队列限制并发数量
- 注意计算密集型任务长时间占用CPU,导致其他请求回复过慢的情况。应该将CPU占用率高的任务提炼成单独的微服务,再配以适当的计算资源(基本+高峰弹性)
IO密集型任务并发时需要思考的问题
- 因为大多数时间会花费在等待IO阻塞上,可以多开一些线程,充分利用CPU
- 注意防止大量线程阻塞,或同时读取数据到内存导致的内存溢出
为什么单机CPU逻辑内核数最多不过20几,网页服务器(tomcat,gunicorn等)默认的同时接受请求数却会达到1000?
默认的网络请求消耗的计算时间其实不高,线程并发虽然会增加每个请求处理的耗时,但一个线程完成任务消耗20ms还是50ms用户并感受不到差别,而且相比起网络IO几百毫秒的延迟几乎可以忽略
Python高并发执行计算密集型任务时的情况
GIL(Global Interceptor Lock)是Python为了防止解释器资源竞争而设立的机制,一个进程只有一个GIL,当有多个线程时,线程竞争GIL,只有获取到GIL的线程才可以运行
由于GIL锁的影响,Python的一个进程永远都只有一个线程正在执行,因此Python的多线程无法利用多CPU的计算环境
想要在Python实现计算密集型任务的高并发,只能依靠多进程,网络应用可以依靠uswgi等网络服务器自带的多worker机制,本地应用可以利用multiprocessing库
因为是多进程而不是多线程,所以CPU和内存溢出比Java更好控制
协程在IO密集型任务时也可以帮助提高并发,但面对计算密集型任务效果并不好
Java高并发执行计算密集ç任务时的情况
启动java程序会固定在jvm上启动一个进程,因此单个java应用的任务并发主要依靠多线程来解决,操作系统会将不同的java线程分配到不同的CPU内核上
单个应用内还可以通过锁核操作防止计算密集型任务对其他任务的影响
Reference
计算机组成原理:CPU、核心,进程、线程,串行、并发、并行
CPU、内存、进程、线程原理
知乎 - Java 多线程如何实现在多 CPU 上分布?(用户【Java技术那些事
】的回答)
浅谈Python和Java的多进程与多线程的异同