进程,线程与CPU之间是如何搭伙儿过小日子的

进程与线程的概念

进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

每个进程拥有完全不同的虚拟地址空间,操作系统内核通Address Translation技术映射到物理地址空间(X86处理器体系架构采用段表+页表进行映射,页表有2级和4级之分,32位系统采用2级页表,64位系统采用4级页表),这让进程有一种幻觉即独占整个内存空间。

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。线程与线程之间是没有隔离的,虽说每个线程有自己的工作栈空间,但是线程A去访问线程B的工作栈空间也是可以做到的。某个线程的行为可能影响到进程内其他的线程。因此,一个线程的崩溃可能引起一系列连锁反应导致其他线程崩溃,最后甚至影响进程的崩溃。因此一些核心线程要尽量保证与其他易出问题的线程的耦合度要低。

而耦合度降低也会带来一些其他问题。比如:某个线程抛出异常没有捕获,对应的线程会崩溃,但是对应进程(JVM虚拟机)并不会随之崩溃。这样既有好处也有坏处,好处是:其他线程不受影响,坏处是:若没有外围的监控,很难察觉到对应的线程是否已经崩溃。

经过多方面概述后,我们知道多进程的程序一定要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。

如果说,基础不够扎实的读者,第一次看到这些内容,或许对进程和线程的概念依旧不会很清晰。别急,待我将进程,线程与CPU之间的关系娓娓道来。

CPU核心数与线程的关系

首先先说一些基础的概念。

一个计算机只有一个CPU,这是勿容置疑的。在不断地学习多线程的知识后,有些朋友已经脑袋里是一堆CPU在计算机里到处乱转了。实际上它只有一个。

但是站在计算机的用户的角度来看,所有的程序确确实实是在同时运行,那么一个CPU是如何让所有程序在我们用户眼里保持同时运行的呢?

CPU的影分身——CPU多核心

多核心:也指单芯片多处理器(CMP)。CMP是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称到处理器)集成和到同一芯片内,各个处理器并行执行不同的进程,这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理。

简而言之,这个理念的核心,就是把多个处理器集中当一个芯片中,实现一个CPU多处理器的效果。而这个多处理器又被称为CPU核心。

核心数,线程数:目前主流的CPU都是多核的。增加核心数目就是为了增加线程数。因为曹祖系统一般是通过线程来执行任务的,一般情况下它们是1:1的对应关系,也就是说四核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。(这个8也就代表着多可并行8个线程)。

进程,线程与CPU之间是如何搭伙儿过小日子的_第1张图片

因此,以后再提到线程数,一定要想到是与CPU的"影分身"——核心数挂钩的。

如何理解资源分配

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。

为了让进程完成一定的工作,进程必须至少包含一个线程。

周所周知,CPU是非常宝贵的。那么如何将它的运算效率发展的淋漓尽致呢?答案是资源分配与处理任务进行解耦。

我们的CPU代表着只会愣头干活的Java程序员,他的代码能力很强。即使他换工作,跳槽,但是经过短时间的适应后,就可以发挥其高效的完成任务能力。

那么如何保证这个强大的程序员,到达一个新环境可以进行快速适应环境呢?答案就是提前进行资源的分配,也就是我们进程的概念。我们的操作系统如果是一个公司,那么一个进程好比是一个部门,它为了解决一类业务而存在。这时我们发现,部门这个名词儿压根儿不会干活儿,对应进程也是如此。但是部门存在的意义依旧是重中之重,有其中两个原因:

  1. 作为公司(操作系统)资源分配的单位。现在公司到了一批电脑,我们往往的分派目标是一个部门,很不会为一个程序员而去独立采购的只说。(站在操作系统分配进程资源的角度上说。你也别跟我说什么公司太子爷。程序的世界没有人情关系,只讲究效率!)。经过操作系统的分配后,进程就有了自己的地址空间。
  2. 让该部门(进程)中的任务(线程)有归属感。所有的线程都知道自己是这个部门的,可以使用该部门的资源。也就是说线程就会使用进程的地址空间。如果学习过JVM,那么就知道运行时数据区的概念。整个运行时数据区就是操作系统对该Java进程分配的一片工作区域。

往往资源分配要提前搞好,也就是上下文环境要提前布置完善。我们可以想象,当一个线程如果获取到了CPU执行权,但是其上下文环境不清不楚,导致CPU无法立即投入战斗,就会产生性能的大幅损耗。

如何理解处理器调度

在刚才的介绍中,我们把操作系统比喻成公司,进程是部门。而一个部门起码得有一个或者多个业务支撑。此时的业务就代表着线程。CPU则是我们的Java开发人员,而我们的开发人员是如何执行这些业务的呢?

Java中调度的基本单位是线程,也就是linux内核线程,也就说轻量级进程,基于抢占式调度。可以通过在新建线程时,获取其优先级而调整该线程在未来抢占CPU内核执行权的概率。但是本质依旧是抢占式调度。

对应到我们的程序中,在线程(任务)多,CPU内核(开发人员)少的时候,站在线程的角度上来说:线程争抢CPU核心来处理我们的任务的。站在CPU的角度,则是随机选择线程进行处理。

是不是脑子里有周星驰拍的唐伯虎点秋香里。饭点到了,一群下人一拥而上,把桶里的饭抢的干干净净的样子了?没抢到的CPU执行权的线程就只能像周星驰演的华安那样,一脸懵逼。

CPU时间片轮转机制(RR调度,并发原理)

也就是说,在多核CPU进行频繁切换的过程中,和进程这个概念就不会再有任何关系了。假如我们操作系统中有3个进程,每个进程包含8个线程。那么一共就有24个线程。在CPU分配核心数去处理资源的时候,对这24个线程是完全一视同仁的。并不会因为你们8个线程是一个进程的,就集中分配多一些核心数去处理。

时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

如果时间片结束时,进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或者结束,则CPU当即进行切换。调度程序所要做的就是一张就绪进程列表,当进程用完它的时间片后,它被移动到队列的末尾。

时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等待。假如进程切换上下文需要5ms,再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU将花费5ms来进行进程切换。此时CPU时间的20%被浪费在了管理开销上。

为了提高CPU效率,我们可以将时间片设为5000ms这时,浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10个交互用户几乎同时按下回车键,那么将发生什么情况?假设所有其他进程都用足它们的时间片的话(5000ms=1s),那么后两个用户起码得等上5s才能获取机会。多数用户无法忍受一条简短命令要5s才能做出响应,同样的问题在一台支持多道程序的个人计算机也会发生。

结论可以总结如下:时间片设置太短会导致过多的进程切换,降低了CPU效率。而设计的太长有可能引起对短的交互请求响应变差。将时间片设置为100ms通常是一个比较合理的折中。

进程调度

在读概念的时候,我们逐渐发现了一些问题。就是时间片轮转调度的单位是进程。这与我们所说的线程是处理器调度的最小单位这一概念是否冲突呢?

 

进程,线程与CPU之间是如何搭伙儿过小日子的_第2张图片们在平时开发时,感觉并没有受CPU核心数限制(明明最大并行线程只有8个,为什么3000多个线程同时跑我们却觉得很正常呢?)这是因为操作系统提供了一种CPU时间片轮转机制。

我们现在为了处理这3000多个线程,有两种实现方案。

  1. 直接把这随机把这8个核心分配给这3000多个线程,也就是让所有线程同时去抢占CPU执行权。

假设是这种实现方法,会出现什么问题。那就是可能会造成线程饥饿。万一有一些线程运气不好,很长一段时间都无法抢到CPU执行权,那么这个进程反馈给我们的状态必然是频繁卡顿。

  1. 就是以为们进程为单位,统一将CPU分配至这个进程中的线程,仅仅让这个进程中的线程进行争夺CPU执行权,那么就可以保证线程饥饿的概率大幅下降,让所有CPU核心集中去做一个进程下的任务。

这样做有什么好处呢?

  1. 大幅度减轻了线程饥饿带来的问题。
  2. 实现一个进程中CPU核心数的可控性。因此CPU密集型与IO密集型业务,都可以根据CPU核心数来计算出更合理的线程池。

基于这个问题的出现,我们引出了以进程为单位进行调度的概念。

我们知道线程是抢占CPU核心进行处理任务的。那么进程是如何分配CPU资源的呢?

系统会维护一张就绪进程列表,其实就是一个先进先出的队列,新来的进程就会被加到队列的末尾,然后每次执行进程调度的时候,都会选择队列的队首进程,让它在CPU上运行一个时间片的时间,不过如果分配的时间片已经消耗光了而进程还在运行,调度程序就会停止该进程的运行,同时把它移到队列的末尾,CPU会被剥夺并分配给队首进程,而如果进程在时间片结束前阻塞或者结束了,则CPU就会进行切换。

如此一来,再次对比直接把这8个核心分配给这3000多个线程的方式。我们发现维护这个进程队列,就免不了要消耗更大的上下文切换成本。但与我们之前所说的那些优点相比,这也是相当值得的。

总结

最后再进行一下点题。回到我们最开始的问题:一个CPU是如何让所有程序在我们用户眼里保持同时运行的呢?

首先,在进程层面,一段较长的时间被CPU被划分为多个时间碎片,然后在这一段时间内并发执行所有进程(来回切换干活),并且这里所有的进程都是排好队的。

在CPU分配到一个进程后,因为超线程技术,使CPU具备并行执行的内置核心数*2的线程数。如果程序中需要减少CPU核心在一个进程中的线程里频繁切换,那么该进程中的线程数要尽量设置的小于该机器的CPU核心数*2。

 

你可能感兴趣的:(多线程)