过程
我们都知道计算机的核心是CPU,承担所有的计算任务;操作系统是计算机的管理者,负责任务调度、资源分配和管理,统领整个计算机硬件;应用程序是运行在操作系统上的具有一定功能的程序。
进程是具有一定独立功能的程序在数据集上的动态执行过程,是操作系统进行资源分配和调度的独立单元,是应用程序运行的载体。过程是一个抽象的概念,从来没有统一的标准定义。
通常,一个过程由三部分组成:程序、数据收集和过程控制块。
程序用于描述进程要完成的功能,是控制进程执行的指令集;
集合是程序执行时需要的数据和工作空间;
程序控制块(PCB)包含进程的描述信息和控制信息,是进程存在的唯一标志。
流程的特点:
动态性:进程是程序的执行过程,是临时的,有生存期,动态生成,动态消亡;
并发性:任何进程都可以与其他进程并发执行;
独立性:进程是系统中用于资源分配和调度的独立单元;
结构:过程由三部分组成:程序、数据和过程控制块。
线
在早期的操作系统中,没有线程的概念。进程是能够拥有资源并独立运行的最小单位,也是程序执行的最小单位。任务调度采用时间片轮换的抢占式调度模式,进程是任务调度的最小单位。每个进程都有自己独立的内存,使得每个进程的内存地址相互隔离。
后来随着计算机的发展,对CPU的要求越来越高,进程间的切换代价昂贵,已经不能满足越来越复杂的程序的要求。所以我发明了线。
它是线程程序执行中的单个顺序控制流,是程序执行流的最小单位,也是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,每个线程共享程序的内存空(也就是自己进程的内存空)。一个标准线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。进程由内存空(代码、数据、进程空、打开的文件)和一个或多个线程组成。
(有些读者看到这里可能会很困惑,觉得这和Java的memory 空模型不一样,但是如果你看过《Java虚拟机深度理解》这本书,你就会恍然大悟。)
如上图,在任务管理器的进程列中,一个字典和一个云笔记是进程,进程下有多个线程执行不同的任务。
任务调度
什么是线程?要理解这个概念,我们需要了解操作系统的一些相关概念。大多数操作系统(如Windows、Linux)采用时间片轮换的抢占式调度方式。
在一个进程中,当一个线程的任务执行了几个毫秒,就会被操作系统的内核(负责管理每个任务)调度。处理器将被硬件计数器中断,线程将被迫暂停,其寄存器将被放入内存。通过检查线程列表,它会决定下一步执行哪个线程,并从内存中回收该线程的寄存器,最后恢复该线程的执行,从而执行下一个任务。
在上述过程中,一个任务被执行的短暂时间称为时间片,任务正在被执行的状态称为运行状态,被挂起的线程任务状态称为就绪状态,即等待属于它的下一个时间片。
这种方式保证了每个线程轮流执行。因为CPU的执行效率非常高,时间片非常短,在任务之间快速切换,给人多任务“同时”的感觉,这就是我们所说的并发(不要以为并发有多高级,它的实现很复杂,但它的概念很简单,就是一句话:多个任务同时执行)。多任务操作流程示意图如下:
操作系统中的任务调度
进程和线程之间的差异
前面讲了进程和线程,大家可能还是会觉得很困惑,觉得很像。事实上,进程与线程密不可分。让我们一起来看看它们:
是线程程序执行的最小单位,而进程是操作系统资源分配的最小单位;
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路径;
进程之间是相互独立的,但是同一个进程中的每个线程共享程序的内存空(包括代码段、数据集、堆等。)和一些进程级资源(比如打开文件和信号),一个进程中的线程对其他进程是不可见的;
和调度切换:线程上下文切换比进程上下文切换快得多。
线程和进程之间的关系示意图:
与流程的资源共享关系
单线程和多线程的关系
总之,线程和进程都是抽象概念。线程是比进程更小的抽象,线程和进程都可以用来实现并发。
在早期的操作系统中,没有线程的概念。进程是能够拥有资源并独立运行的最小单位,也是程序执行的最小单位。相当于一个进程中只有一个线程,进程本身就是一个线程。因此,线程有时被称为轻量级进程(LWP)。
早期的操作系统只有进程,没有线程。
后来随着计算机的发展,多任务间的上下文切换效率越来越高,于是抽象出一个更小的概念——线程。通常,一个进程会有多个(或一个)线程。
线程的出现允许一个进程拥有多个线程。
多线程和多核
上述时间片轮换的调度方式是指一个任务经过一小段时间后强制暂停执行下一个任务,每个任务依次执行。很多操作系统书都说“同一时间只执行一个任务”。那么可能有人要问双核处理器了?两个核心不是同时运行吗?
其实“同时只执行一个任务”这句话是不准确的,至少是不完整的。在多核处理器的情况下,线程如何执行?这需要了解内核线程。
多核(core)处理器是指在一个处理器上集成多个计算核心来提高计算能力,即有多个处理核心进行真正的并行计算,每个处理核心对应一个内核线程。
内核线程(KLT)是由操作系统内核直接支持的线程。这种线程由内核切换,内核通过操作调度器调度线程,负责将线程的任务映射到各个处理器。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。
现在的电脑一般都是双核四线程,四核八线程。超线程技术是将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以操作系统中看到的CPU数量是实际物理CPU的两倍。例如,如果您的计算机是双核四线程,请打开任务管理器\\性能查看四个CPU的监视器,打开四核八线程查看八个CPU的监视器。
在Windows8中查看四核线程的结果
超线程技术是利用特殊的硬件指令将一个物理芯片模拟成两个逻辑处理核心,使单个处理器可以使用线程级并行计算,从而兼容多线程操作系统和软件,减少CPU的空闲时间,提高CPU的运行效率。这种超线程技术(如双核四线程)是由处理器的硬件决定的,也需要操作系统的支持才能在电脑中显示。
程序一般不直接使用内核线程,而是使用内核线程的一个高级接口——轻量级进程(LWP)。轻量级进程就是我们通常所说的线程,也叫用户线程。由于每个轻量级进程都由一个内核线程支持,所以只有首先支持内核线程,才能有轻量级进程。用户与内核线程的对应关系有三种模型:一对一模型、多对一模型和多对多模型。这里以四个内核线程和三个用户线程为例来说明这三种模型。
一对一模型
对于一对一模型,一个用户线程唯一对应一个内核线程(反之不一定成立,内核线程也不一定有对应的用户线程)。这样,如果CPU不采用超线程技术(如四核四线程计算机),一个用户线程唯一映射到一个物理CPU的内核线程,线程间的并发就是真并发。一对一的模式使得用户线程拥有和内核线程一样的优势。当一个线程因为某种原因被阻塞时,其他线程的执行不会受到影响。在这里,一对一模型也可以使多线程程序在多处理器系统上执行得更好。
但是一对一模式也有两个缺点:
很多操作系统限制内核线程的数量,所以一对一的模式会限制用户线程的数量;
当调度多个操作系统内核线程时,上下文切换开销大,导致用户线程执行效率下降。
一对一模型
多对一模型
多对一模型将多个用户线程映射到一个内核线程,线程之间的切换由用户态代码完成,所以系统内核感受不到线程的实现。用户线程的建立、同步和销毁都在用户态完成,不需要内核的干预。所以相对于一对一模型,多对一模型的线程上下文切换速度要快很多;此外,多对一模型对用户线程的数量几乎没有限制。
但是多对一模型也有两个缺点:
如果其中一个用户线程被阻塞,所有其他线程将无法执行,因为此时内核线程被阻塞。
在多处理器系统上,处理器数量的增加不会显著提高多对一模型的线程性能,因为所有用户线程都映射到一个处理器。
多对一模型
多对多模型
多对多模型结合了一对一模型和多对一模型的优点,将多个用户线程映射到多个内核线程。线程库负责在可用的可调度实体上调度用户线程,这使得线程的上下文切换非常快,因为它避免了系统调用。然而,它增加了复杂性和优先级反转的可能性,以及在用户模式调度程序和内核调度程序之间没有广泛(且昂贵)协调的情况下的次优调度。
多对多模型的优点是:
一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有其他内核线程被调度执行;
多对多模型对用户线程的数量没有限制;
在多处理器操作系统中,多对多模型线程也能获得一定的性能提升,但提升率没有一对一模型高。
多对多模型
多对多模型主要用于流行的操作系统。
查看进程和线程
应用程序可以是多线程或多进程的。怎么查?在Windows下,我们只能通过打开任务管理器来查看一个应用的进程和线程的数量。按“Ctrl+Alt+Del”或右键单击快捷工具栏打开任务管理器。
查看进程和线程的数量:
查看线程和进程的数量
在“Processes”选项卡下,我们可以看到应用程序中包含的线程数量。如果一个应用程序有多个进程,我们可以看到每个进程。比如谷歌的Chrome浏览器就有多个进程。同时,如果打开一个应用程序的多个实例,就会有多个进程。如上图所示,如果我打开两个cmd窗口,就会有两个cmd进程。如果看不到线程数,可以再次点击“查看\\选择栏目”菜单添加栏目收听。
检查CPU和内存使用情况:
在performance选项卡中,我们可以查看CPU和内存使用情况,还可以根据CPU使用情况记录的监视器数量查看逻辑处理核心的数量。比如我的双核四线程电脑,有四台显示器。
检查CPU和内存使用情况。
线程的生命周期
当线程数小于处理器数时,线程并发为真,不同的线程运行在不同的处理器上。但是,当线程数量大于处理器数量时,线程的并发性就会受到阻碍。这时候就不是真正的并发了,因为至少一个处理器会运行多个线程。
并发是单个处理器运行多个线程时的模拟状态。操作系统使用时间片轮换来依次执行每个线程。现在几乎所有的现代操作系统都采用了时间片轮换的抢占式调度方式,比如我们熟悉的Unix、Linux、Windows、macOS等流行的操作系统。
我们知道线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态:创建、就绪、运行、阻塞(等待)和退出。早期进程相当于只有单线程的当前进程,所以当前多线程有五种状态,当前多线程的生命周期和早期进程差不多。
早期流程的生命周期
运行过程中有三种状态:就绪、运行、阻塞、创建和退出状态描述了创建和退出一个进程的过程。
创建:流程正在创建中,还不能运行。操作系统在创建进程时要做的工作包括分配和建立进程控制块表项、建立资源表和分配资源、加载程序和建立地址空;
Ready:时间片已经用完,这个线程被强制挂起,等待下一个属于它的时间片;
Run:该线程正在执行并占用时间片;
阻塞:也叫等待状态,等待一个事件(比如IO或者另一个线程)执行完;
退出:进程已经结束,所以也叫结束状态,释放操作系统分配的资源。
线程的生命周期
Create:创建一个新线程,等待线程被调用执行;
Ready:时间片已经用完,这个线程被强制挂起,等待下一个属于它的时间片;
Run:该线程正在执行并占用时间片;
阻塞:也叫等待状态,等待一个事件(比如IO或者另一个线程)执行完;
退出:当一个线程完成一个任务或者其他终止条件发生时,线程终止,进入退出状态,释放线程分配的资源。
协同程序
协同进程,英文Coroutines,是一种基于线程的存在,但比线程更轻量级。这种由程序员自己写程序管理的轻量级线程叫做“user 空 inter-thread”,对内核是不可见的。
因为它们是自主开发的异步任务,所以很多人更愿意称之为纤程,或者GreenThread。正如一个进程可以有多个线程一样,一个线程也可以有多个协程。
该过程的目的
在传统的J2EE系统中,每个请求占用一个线程来完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的运行时间。如果遇到一个耗时的I/O行为,整个系统的吞吐量会立刻下降,因为这个时候线程总是被阻塞的。如果有很多线程,就会有很多线程闲置(等待线程执行完),导致资源应用不完整。
最常见的例子是JDBC(同步阻塞),这就是为什么很多人说数据库是瓶颈。这里的耗时实际上是让CPU一直等待I/O返回。说白了就是线程处于空 turn状态,根本不用CPU做运算。此外,过多的线程也会带来更多的ContextSwitch开销。
对于以上问题,现阶段业界比较流行的解决方案之一是单线程加异步回调。其代表是node.js和Vert.x,Java的新秀。
协同学的目的是通过放弃当前协同学调度并在有长时间I/O操作时执行下一个任务来消除ContextSwitch的开销。
谢城特色
操作系统负责调度线程切换,用户负责调度进程,减少了上下文切换,提高了效率。
线程的默认堆栈大小是1M,而协程更小,接近1K。因此,可以在同一个内存中打开更多的协程。
因为是在同一个线程上,所以可以避免竞争,使用锁。
适用于需要大量并发的阻塞场景。但不适合计算量大的多线程。在这种情况下,不如用实用的线程来解决。
协同过程原理
当发生IO拥塞时,由协调调度器进行调度。通过立即让出数据流,并将数据记录在当前堆栈上,堆栈在拥塞后立即由线程恢复,拥塞的结果放在该线程上运行。看起来好像和写同步代码没什么区别。整个进程可以称为协程,在协程的调度中运行的线程称为纤程。比如Golang中的go关键字,其实就是负责打开一个纤程,让func逻辑在上面运行。
因为协调过程的暂停完全由程序控制,它发生在用户状态;但是,线程的阻塞状态是由操作系统的内核切换的,这发生在内核状态。
所以协程的开销远小于线程,所以ContextSwitch上没有开销。
进程和线程的比较
比较线程协调占用资源的初始单位是1MB,固定的初始通常是2KB,可以根据需要增加。调度属于OS内核,切换由用户完成。开销包括模式切换(从用户模式切换到内核模式)、刷新16个寄存器、PC、SP,…诸如此类。只有三个寄存器被修改——PC/SP/DX。性能问题占用太多资源,频繁的创建和销毁会带来严重的性能问题。资源占用小,但不会带来严重的性能问题。数据同步需要锁定和其他机制来确保数据的一致性和可见性。没有多线程锁机制,因为只有一个线程,不存在同时写变量的冲突。在协调过程中只需要判断控制共享资源时的状态,所以执行效率远高于多线程。