一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)

进程与线程

进程

何为进程

进程通俗的说就是我们计算机中一个个正在运行中的程序的抽象出来的概念,一个正在运行的程序就是一个进程即程序的一个执行实例。

进程是担当分配系统资源(CPU时间,内存,打开的文件)的实体也是基本单位。

程序的本质是一个程序员用代码编写的一个存储在硬盘的静态文件(包含指令和数据),这个文件经过编译之后就生成了一个可以执行的二进制文件。当我们运行这个二进制可执行文件之后,CPU会把这个文件加载进内存,然后执行这个程序中的每一条指令。这个正在运行中的程序就被称为进程,进程也就是这个程序的一次执行过程(从创建到死亡)

在操作系统中,是用进程控制块process control block,PCB)数据结构来描述进程的。PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。PCB是通过链表方式(进程的状态变化是比较频繁的
,链表可以灵活地插入删除)组织的,把具有相同状态的进程链在一起,组成各种队列

  • 将所有处于就绪状态的进程链在一起,称为就绪队列
  • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列
  • 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。

进程的状态

CPU执行的速度是很快的,但是一个CPU(单核)同一时间只能执行一个进程。但是一旦一个进程在执行某一个任务(比如磁盘IO)时需要时间比较长,那么CPU会一直等这个进程执行完这个任务吗?

CPU是不会阻塞等待这个进程的任务执行完毕的,CPU可以管理多个进程,交替地执行它们(所以各个进程之间也是共享CPU资源的)。对于一个支持多进程的系统,CPU 会从一个进程快速切换至另一个进程(这个过程叫做上下文切换),其间每个进程各运行几十或几百个毫秒。虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发。只需要在CPU切换进程执行的时候,记录下当前进程中运行的状态信息,以备下次切换回来的时候可以恢复执行。因此进程有着「运行 - 暂停 - 运行」的活动规律。一般说来CPU都是支持多进程的,所以一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的。它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使它暂停的原因消失后,它又进入准备运行状态。

所以,在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。

所以,在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第1张图片

上图中各个状态的意义:

  • 运行状态(Running):该时刻进程占用 CPU;
  • 就绪状态(Ready):可运行,由于其他进程处于运行状态而暂时停止运行;
  • 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;

当然,进程还有另外两个基本状态:

  • 创建状态(new):进程正在被创建时的状态;
  • 结束状态(Exit):进程正在从系统中消失时的状态;

于是,一个完整的进程状态的变迁如下图:

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第2张图片

再来详细说明一下进程的状态变迁:

  • NULL -> 创建状态:一个新进程被创建时的第一个状态;
  • 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
  • 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
  • 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
  • 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
  • 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
  • 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;

如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间,显然不是我们所希望的,毕竟物理内存空间是有限的,被阻塞状态的进程占用着物理内存就一种浪费物理内存的行为。

所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第3张图片

那么,就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。

另外,挂起状态可以分为两种:

  • 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;(这种挂起状态是因为操作系统会把处于阻塞状态的进程的物理内存空间换出到磁盘导致的)
  • 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;(这种是人为干预的,因为处于就绪状态的进程并不会用到物理内存空间)

这两种挂起状态加上前面的五种状态,就变成了七种进程的状态变迁,见如下图:

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第4张图片

导致进程挂起的原因不只是因为进程所使用的内存空间不在物理内存,还包括如下情况:

  • 通过 sleep 让进程间歇性挂起,其工作原理是设置一个定时器,到期后唤醒进程。
  • 用户希望挂起一个程序的执行,比如在 Linux 中用 Ctrl+Z 挂起进程;

进程的上下文切换

大多数操作系统都是多任务,通常支持大于 CPU 数量的任务同时运行。实际上,这些任务只是并发地在CPU上运行。

任务是交给 CPU 运行的,那么在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。

所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器

CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)。程序计数器则是用来存储 CPU 正在执行的指令位置或者即将执行的下一条指令位置。

所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文

CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器的值)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

系统内核会存储保存每一个切换下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换

那么什么是进程上下文切换呢?

各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换

进程是由内核管理和调度的,所以进程的切换只能发生在内核态。所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第5张图片

大家需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。

发生进程上下文切换有哪些场景?

  • 为了保证所有进程可以得到公平调度,CPU的运行时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
  • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
  • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
  • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
  • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

线程

在早期的操作系统中都是以进程作为独立运行的基本单位,后面又提出了更小的能独立运行的基本单位,也就是线程。

为什么使用线程?

因为如果只是使用单进程执行任务的话,如果任务中出现了阻塞(比如需要进行磁盘IO)那么就会进程就会阻塞在这里,导致后面的任务无法执行,影响资源的使用效率。

那改进成多进程的方式:

一个进程执行一个程序中的子任务。对于多进程的这种方式,我们就需要考虑进程之间如何通信,共享数据,而且维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息;

解决方案就是需要有一种新的实体,满足以下特性:

  • 实体之间可以并发运行;
  • 实体之间共享相同的地址空间(便于通信);

这个新的实体,就是线程( Thread ),线程之间可以并发运行且共享相同的地址空间。

什么是线程?

线程是进程当中的一条执行流程。 线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源和执行调度分开,各个线程既可以共享进程资源(内存地址、文件IO等),又可以独立调度,线程是CPU调度的基本单位

同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。每个进程至少有一个线程存在,即主线程。

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第6张图片

线程的优缺点?

线程的优点:

  • 一个进程中可以同时存在多个线程;
  • 各个线程之间可以并发执行;
  • 各个线程之间可以共享地址空间和文件等资源;

线程的缺点:

  • 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(这里是针对 C/C++ 语言,Java语言中的线程奔溃不会造成进程崩溃)

线程的状态

Java中的线程状态定义在Thread类中的一个内部枚举类,共有6种状态。

public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
}

状态的转换如图所示:

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第7张图片

新建(NEW)

创建后尚未启动的线程处于这个状态即还没有被调用start()时候的状态。这个时候从本质上仅是一个Thread对象。

可运行(RUNNABLE)

Runnable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也有可能正在等待CPU为它分配执行时间。当线程被调用了start(),且处于等待操作系统分配资源(如CPU)、等待IO连接、正在运行状态,即表示Ready状态和Running状态。注意不一定被调用了start()立刻会改变状态,还有一些准备工作,这个时候的状态是不确定的。

无限期等待(Waiting)

处于这种状态的线程不会被分配CPU执行时间,它们要等待被其它线程显示地唤醒。以下方法会让线程陷入无限期等待状态:

1)没有设置timeout参数的Object.wait()方法。

2)没有设置timeout参数的Thread.join()方法。

3)LockSupport.park()方法。

如果没有被唤醒或等待的线程没有结束,那么将一直等待,当前状态的线程不会被分配CPU资源和持有锁

限期等待(Timed Waiting)

处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其它线程显示地唤醒,在一定时间之后它们会由系统自动的唤醒。以下方法会让线程进入TIMED_WAITING限期等待状态:

1)Thread.sleep()方法

2)设置了timeout参数的Object.wait()方法

3)设置了timeout参数的Thread.join()方法

4)LockSupport.parkNanos()方法

5)LockSupport.parkUntil()方法

阻塞(Blocked)

线程被阻塞了,说明一下和等待状态的区别

阻塞状态:在等待着获取到一个排他锁,这个事件在另外一个线程放弃这个锁的时候发生

等待状态:在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

结束(Terminated)

已终止的线程,执行完了run()方法。其实这只是Java语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java代码看到的线程状态而已。

具体一点状态转换如下图(出入的地方以上图为准):

一文带你彻底理解进程与线程(包含生命周期的状态转换以及异同点)_第8张图片

其中锁池状态就是阻塞状态,当运行中的线程试图获取一把已被占有的锁时就会进入阻塞状态,直到获得这个锁才会进入可运行状态。所以等待队列其实直接连到可运行状态。

线程的状态和进程的基本相同,因为都具有五种基本状态(新建 就绪 运行 阻塞 死亡)。

线程与进程的比较

  1. 定义

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配(也是基本单位)和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位. 线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

  1. 关系

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

  1. 区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

线程与进程的比较如下:

  • 简而言之,一个程序至少有一个进程,一个进程至少有一个线程。

  • 线程的划分尺度小于进程,使得多线程程序的并发性高。

  • 进程在执行过程中拥有独立的内存单元,拥有一个完整的资源平台;而多个线程共享内存,只独享必不可少的资源(如寄存器和栈),从而极大地提高了程序的运行效率。

  • 进程是资源(包括内存、打开的文件等)分配的基本单位,线程是 CPU 调度的基本单位;

  • 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。**但是线程不能够独立执行,**必须依存在应用程序中,由应用程序提供多个线程执行控制。

  • 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

  • 线程能减少并发执行的时间和空间开销;

对于线程相比进程能减少开销,体现在:

  • 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
  • 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
  • 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
  • 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;

所以,不管是时间效率,还是空间效率线程比进程都要高。

线程的上下文切换

在前面我们知道了,线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位

所以,所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。

对于线程和进程,我们可以这么理解:

  • 当进程只有一个线程时,可以认为进程就等于线程;
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;

另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

线程上下文切换的是什么?

这还得看线程是不是属于同一个进程:

  • 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
  • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

所以,线程的上下文切换相比进程,开销要小很多。

你可能感兴趣的:(java,开发语言)