进程就是一个在内存中运行的应用程序,进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的一块内存空间用来拥有代码、打开的文件资源、数据资源。
进程有五种状态:创建、就绪、阻塞、运行、关闭,转换关系如下:
线程是进程中的一个最小的执行任务(执行单元或控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。与进程不同的是同类的多个线程共享进程(所拥有的全部资源)的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
进程 | 线程 | |
根本区别 | 操作系统资源分配的基本单位 | 处理器任务调度和执行的基本单位 |
资源开销 | 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销; | 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 |
包含关系 | 如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的 | 线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 |
内存分配 | 同一进程的线程共享本进程的地址空间和资源, | 进程之间的地址空间和资源是相互独立的 |
影响关系 | 一个进程崩溃后,在保护模式下不会对其他进程产生影响 | 一个线程崩溃则所在进程终止,所以多进程要比多线程健壮 |
执行过程 | 每个独立的进程有程序运行的入口、顺序执行序列和程序出口。 | 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 |
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
从 JVM 角度说进程和线程之间的关系(重要)
Java 内存区域:
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈,也就是说程序计数器、虚拟机栈、本地方法栈都是线程私有的:
作用 | 线程私有/共享 | ||
程序计数器 | 1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到的位置。需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。 | 私有 | |
虚拟机栈 | 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 | 为了保证线程中的局部变量不被别的线程访问到 | |
本地方法栈 | 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 | ||
堆 | 进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存) | 共享 | |
方法区 | 主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 |
协程(Coroutines)是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。它就是一个函数,一个特殊的函数——可以在某个地方挂起,并且可以重新在挂起处继续运行。所以说,协程与进程、线程相比,不是一个维度的概念。
一个进程可以包含多个线程,一个线程也可以包含多个协程,也就是说,一个线程内可以有多个那样的特殊函数在运行。
一个线程内的多个协程的运行一定是串行的。如果有多核CPU的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内的多个协程却绝对串行的,无论有多少个CPU(核)。协程本质就是一个函数。一个线程内可以运行多个函数,但是这些函数都是串行运行的。当一个协程运行时,其他协程必须挂起。协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。协程与进程、线程相比不是一个维度的概念
4 协程与进程一样,它们的切换都存在上下文切换问题。
进程、线程、协程上下文切换的比较:
进程 | 线程 | 协程 | |
切换者(管理者) | 操作系统 | 操作系统 | 用户(编程者/应用程序) |
切换时机 | 根据操作系统自己的切换策略,用户不感知 | 根据操作系统自己的切换策略,用户不感知 | 用户自己(程序)决定 |
切换内容 | 页全局目录
内核栈 硬件上下文 |
内核栈
硬件上下文 |
硬件上下文 |
切换内容的保存 | 保存于内核栈中 | 保存于内核栈中 | 保存于用户自己的变量(用户栈或者堆) |
切换过程 | 用户态-内核态-用户态 | 用户态-内核态-用户态 | 用户态(没有陷入内核态) |
切换效率 | 低 | 中 | 高 |
协程的使用场景
一个线程内的多个协程是串行执行的,不能利用多核,所以,显然,协程不适合计算密集型的场景。协程适合I/O 阻塞型。I/O本身就是阻塞型的(相较于CPU的时间世界而言)。就目前而言,无论I/O的速度多快,也比不上CPU的速度,所以一个I/O相关的程序,当其在进行I/O操作时候CPU实际上是空闲的。
假设1个线程有5个I/O子程序要处理。如果绝对串行化,当其中一个I/O阻塞时,其他4个I/O并不能得到执行,5个I/O必须排队等待,依次处理,当前面一个I/O阻塞时,后面4个始终处于等待状态:
而协程能比较好地处理这个问题,当一个协程(特殊子进程)阻塞时,它可以切换到其他没有阻塞的协程上去继续执行,这样就能得到比较高的效率:
上面举的例子是5个I/O处理,可见I/O阻塞时,利用协程来处理,切换效率比较高。但假设每秒500、50000、5000000个I/O呢?已经达到了“I/O密集型”的程度,因为协程没有利用多核的能力,这种情况协程将无能为力,需要使用“多进程+协程”。
Neutorn可以归类为I/O阻塞型,所以在Neutorn的代码中处处可见协程的相关代码。