从根上理解高性能、高并发(七):深刻操做系统,一文读懂进程、线程、协程

本文引用了“一文读懂什么是进程、线程、协程”一文的主要内容,感谢原做者的无私分享。php

一、系列文章引言

1.1 文章目的

做为即时通信技术的开发者来讲,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具备这些技术特征的技术框架好比:Java的Netty、Php的workman、Go的gnet等熟练掌握。但真正到了面视或者技术实践过程当中遇到没法释怀的疑惑时,方知自已所掌握的不过是皮毛。html

返璞归真、回归本质,这些技术特征背后的底层原理究竟是什么?如何能通俗易懂、绝不费力真正透彻理解这些技术背后的原理,正是《从根上理解高性能、高并发》系列文章所要分享的。node

1.2 文章源起

我整理了至关多有关IM、消息推送等即时通信技术相关的资源和文章,从最开始的开源IM框架MobileIMSDK,到网络编程经典巨著《TCP/IP详解》的在线版本,再到IM开发纲领性文章《新手入门一篇就够:从零开发移动端IM》,以及网络编程由浅到深的《网络编程懒人入门》、《脑残式网络编程入门》、《高性能网络编程》、《鲜为人知的网络编程》系列文章。laravel

越往知识的深处走,越以为对即时通信技术了解的太少。因而后来,为了让开发者门更好地从基础电信技术的角度理解网络(尤为移动网络)特性,我跨专业收集整理了《IM开发者的零基础通讯技术入门》系列高阶文章。这系列文章已然是普通即时通信开发者的网络通讯技术知识边界,加上以前这些网络编程资料,解决网络通讯方面的知识盲点基本够用了。git

对于即时通信IM这种系统的开发来讲,网络通讯知识确实很是重要,但回归到技术本质,实现网络通讯自己的这些技术特征:包括上面提到的线程池、零拷贝、多路复用、事件驱动等等,它们的本质是什么?底层原理又是怎样?这就是整理本系列文章的目的,但愿对你有用。程序员

1.3 文章目录

从根上理解高性能、高并发(一):深刻计算机底层,理解线程与线程池
从根上理解高性能、高并发(二):深刻操做系统,理解I/O与零拷贝技术
从根上理解高性能、高并发(三):深刻操做系统,完全理解I/O多路复用
从根上理解高性能、高并发(四):深刻操做系统,完全理解同步与异步
从根上理解高性能、高并发(五):深刻操做系统,理解高并发中的协程
从根上理解高性能、高并发(六):通俗易懂,高性能服务器究竟是如何实现的
从根上理解高性能、高并发(七):深刻操做系统,一文读懂进程、线程、协程》(* 本文)

1.4 本篇概述

本系文章中的《从根上理解高性能、高并发(一):深刻计算机底层,理解线程与线程池》、《从根上理解高性能、高并发(五):深刻操做系统,理解高并发中的协程》两篇文章,虽然都有涉及到进程、线程、协程知识,但感受仍是不够系统,系统单独整理了本文,但愿将这方面的知识系统梳理和总结,达到完全解惑的目的。github

本篇是本系列文章的临时续篇,本篇将由浅入深,总结进程、线程、协程这3个技术概念,将3者的技术原理、用途、关系进行了系统梳理和总结,但愿有助于解决你这方面的技术困惑。数据库

本文已同步发布于“即时通信技术圈”公众号,欢迎关注。公众号上的连接是:点此进入编程

二、本文原做者

本文内容引用了“一文读懂什么是进程、线程、协程”一文的主要内容(原文地址已没法查到),原做者:luoweifu,毕业于江西农业大学,现就任于阿里钉钉。我的博客地址:https://blog.csdn.net/luoweifu浏览器

三、什么是进程?

3.1 基本常识

咱们须要明确一下几个常识:

  • 1)计算机的核心是CPU,它承担了全部的计算任务;
  • 2)操做系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;
  • 3)应用程序则是具备某种功能的程序,程序是运行于操做系统之上的。

进程是一个具备必定独立功能的程序在一个数据集上的一次动态执行的过程,是操做系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,历来没有统一的标准定义。

进程通常由程序、数据集合和进程控制块三部分组成:

  • 1)程序用于描述进程要完成的功能,是控制进程执行的指令集;
  • 2)数据集合是程序在执行时所须要的数据和工做区;
  • 3)程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的惟一标志。

进程具备的特征:

  • 1)动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
  • 2)并发性:任何进程均可以同其余进程一块儿并发执行;
  • 3)独立性:进程是系统进行资源分配和调度的一个独立单位;
  • 4)结构性:进程由程序、数据和进程控制块三部分组成。

3.2 为何要有多进程?

多进程目的:提升cpu的使用率。

一个例子:一个用户如今既想使用打印机,又想玩游戏。

假设只有一个进程(先不谈多线程):

从操做系统的层面看,咱们使用打印机的步骤有以下:

  • 1)使用CPU执行程序,去硬盘读取须要打印的文件,而后CPU会长时间的等待,直到硬盘读写完成;
  • 2)使用CPU执行程序,让打印机打印这些内容,而后CPU会长时间的等待,等待打印结束。

在这样的状况下:其实CPU的使用率其实很是的低。

打印一个文件从头至尾须要的时间多是1分钟,而cpu使用的时间总和可能加起来只有几秒钟。然后面若是单进程执行游戏的程序的时候,CPU也一样会有大量的空闲时间。

使用多进程后:

当CPU在等待硬盘读写文件,或者在等待打印机打印的时候,CPU能够去执行游戏的程序,这样CPU就能尽量高的提升使用率。

再具体一点说,其实也提升了效率。由于在等待打印机的时候,这时候显卡也是闲置的,若是用多进程并行的话,游戏进程彻底能够并行使用显卡,而且与打印机之间也不会互相影响。

3.3 小结一下

进程,直观点说:保存在硬盘上的程序运行之后,会在内存空间里造成一个独立的内存体,这个内存体有本身独立的地址空间,有本身的堆,上级挂靠单位是操做系统。

操做系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。

四、什么是线程?

4.1 基本常识

早期:操做系统中并无线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每一个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。

后来:随着计算机的发展,对CPU的要求愈来愈高,进程之间的切换开销较大,已经没法知足愈来愈复杂的程序的要求了。因而就发明了线程。

线程是程序执行中一个单一的顺序控制流程:

  • 1)是程序执行流的最小单元;
  • 2)是处理器调度和分派的基本单位。

一个进程能够有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

PS:读到这里可能有的读者迷糊,感受这和Java的内存空间模型不太同样,但若是你深刻的读过《深刻理解Java虚拟机》这本书的话你就会恍然大悟。

如上图所示:在任务管理器的进程一栏里,有道词典和有道云笔记就是进程,而在进程下又有着多个执行不一样任务的线程。

4.2 任务调度

线程是什么?要理解这个概念,须要先了解一下操做系统的一些相关概念。

大部分操做系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式。

在一个进程中:当一个线程任务执行几毫秒后,会由操做系统的内核(负责管理各个任务)进行调度,经过硬件的计数器中断处理器,让该线程强制暂停并将该线程的寄存器放入内存中,经过查看线程列表决定接下来执行哪个线程,并从内存中恢复该线程的寄存器,最后恢复该线程的执行,从而去执行下一个任务。

上述过程当中:任务执行的那一小段时间叫作时间片,任务正在执行时的状态叫运行状态,被暂停的线程任务状态叫作就绪状态,意为等待下一个属于它的时间片的到来。

这种方式保证了每一个线程轮流执行,因为CPU的执行效率很是高,时间片很是短,在各个任务之间快速地切换,给人的感受就是多个任务在“同时进行”,这也就是咱们所说的并发(别以为并发有多高深,它的实现很复杂,但它的概念很简单,就是一句话:多个任务同时执行)。

多任务运行过程的示意图以下:

▲ 操做系统中的任务调度

4.3 进程与线程的区别

前面讲了进程与线程,但可能你还以为迷糊,感受他们很相似。的确,进程与线程有着千丝万缕的关系。

下面就让咱们一块儿来理一理:

  • 1)线程是程序执行的最小单位,而进程是操做系统分配资源的最小单位;
  • 2)一个进程由一个或多个线程组成,线程是一个进程中代码的不一样执行路线;
  • 3)进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
  • 4)线程上下文切换比进程上下文切换要快得多。

如下线程与进程关系的示意图。

▲ 进程与线程的资源共享关系

▲ 单线程与多线程的关系

总之:线程和进程都是一种抽象的概念,线程是一种比进程更小的抽象,线程和进程均可用于实现并发。

在早期的操做系统中并无线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。它至关于一个进程里只有一个线程,进程自己就是线程。因此线程有时被称为轻量级进程(Lightweight Process,LWP)。

▲ 早期的操做系统只有进程,没有线程

后来:随着计算机的发展,对多个任务之间上下文切换的效率要求愈来愈高,就抽象出一个更小的概念——线程,通常一个进程会有多个(也能够是一个)线程。

▲ 线程的出现,使得一个进程能够有多个线程

4.4 多线程与多核

上面提到的时间片轮转的调度方式说一个任务执行一小段时间后强制暂停去执行下一个任务,每一个任务轮流执行。不少操做系统的书都说“同一时间点只有一个任务在执行”。

那有人可能就要问双核处理器呢?难道两个核不是同时运行吗?

其实:“同一时间点只有一个任务在执行”这句话是不许确的,至少它是不全面的。那多核处理器的状况下,线程是怎样执行呢?这就须要了解内核线程。

_多核(心)处理器是指:_在一个处理器上集成多个运算核心从而提升计算能力,也就是有多个真正并行计算的处理核心,每个处理核心对应一个内核线程。

内核线程(Kernel Thread,KLT):就是直接由操做系统内核支持的线程,这种线程由内核来完成线程切换,内核经过操做调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。

通常一个处理核心对应一个内核线程,好比单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。

如今的电脑通常是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,因此在操做系统中看到的CPU数量是实际物理CPU数量的两倍,如你的电脑是双核四线程,打开“任务管理器 -> 性能”能够看到4个CPU的监视器,四核八线程能够看到8个CPU的监视器。

▲ 双核四线程在Windows8下查看的结果

超线程技术:就是利用特殊的硬件指令,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都能使用线程级并行计算,进而兼容多线程操做系统和软件,减小了CPU的闲置时间,提升的CPU的运行效率。这种超线程技术(如双核四线程)由处理器硬件的决定,同时也须要操做系统的支持才能在计算机中表现出来。

程序通常不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Lightweight Process,LWP),轻量级进程就是咱们一般意义上所讲的线程,也被叫作用户线程。

因为每一个轻量级进程都由一个内核线程支持,所以只有先支持内核线程,才能有轻量级进程。

用户线程与内核线程的对应关系有三种模型:

  • 1)一对一模型;
  • 2)多对一模型;
  • 3)多对多模型。

在下面的文字中,将以4个内核线程、3个用户线程为例对三种模型进行说明。

4.5 一对一模型

对于一对一模型来讲:一个用户线程就惟一地对应一个内核线程(反过来不必定成立,一个内核线程不必定有对应的用户线程)。

这样:若是CPU没有采用超线程技术(如四核四线程的计算机),一个用户线程就惟一地映射到一个物理CPU的内核线程,线程之间的并发是真正的并发。

一对一模型使用户线程具备与内核线程同样的优势:一个线程因某种缘由阻塞时其余线程的执行不受影响(此处,一对一模型也可让多线程程序在多处理器的系统上有更好的表现)。

但一对一模型也有两个缺点:

  • 1)许多操做系统限制了内核线程的数量,所以一对一模型会使用户线程的数量受到限制;
  • 2)许多操做系统内核线程调度时,上下文切换的开销较大,致使用户线程的执行效率降低。

▲ 一对一模型

4.6 多对一模型

多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,系统内核感觉不到线程的实现方式。用户线程的创建、同步、销毁等都在用户态中完成,不须要内核的介入。

所以,相对一对一模型:

  • 1)多对一模型的线程上下文切换速度要快许多;
  • 2)多对一模型对用户线程的数量几乎无限制。

但多对一模型也有两个缺点:

  • 1)若是其中一个用户线程阻塞,那么其它全部线程都将没法执行,由于此时内核线程也随之阻塞了;
  • 2)在多处理器系统上,处理器数量的增长对多对一模型的线程性能不会有明显的增长,由于全部的用户线程都映射到一个处理器上了。

▲ 多对一模型

4.7 多对多模型

多对多模型结合了一对一模型和多对一模型的优势:将多个用户线程映射到多个内核线程上,由线程库负责在可用的可调度实体上调度用户线程。

这使得线程的上下文切换很是快:由于它避免了系统调用。可是增长了复杂性和优先级倒置的可能性,以及在用户态调度程序和内核调度程序之间没有普遍(且高昂)协调的次优调度。

多对多模型的优势有:

  • 1)一个用户线程的阻塞不会致使全部线程的阻塞,由于此时还有别的内核线程被调度来执行;
  • 2)多对多模型对用户线程的数量没有限制;
  • 3)在多处理器的操做系统中,多对多模型的线程也能获得必定的性能提高,但提高的幅度不如一对一模型的高。

▲ 多对多模型

在如今流行的操做系统中,大都采用多对多的模型。

4.8 查看进程与线程

一个应用程序多是多线程的,也多是多进程的,如何查看呢?

在Windows下咱们只须打开任务管理器就能查看一个应用程序的进程和线程数。按“_Ctrl+Alt+Del_”或右键快捷工具栏打开任务管理器。

查看进程数和线程数:

在“进程”选项卡下,咱们能够看到一个应用程序包含的线程数。

若是一个应用程序有多个进程,咱们能看到每个进程,如在上图中,Google的Chrome浏览器就有多个进程。

同时,若是打开了一个应用程序的多个实例也会有多个进程,如上图中我打开了两个cmd窗口,就有两个cmd进程。若是看不到线程数这一列,能够再点击“查看 -> 选择列”菜单,增长监听的列。

查看CPU和内存的使用率:在性能选项卡中,咱们能够查看CPU和内存的使用率,根据CPU使用记录的监视器的个数还能看出逻辑处理核心的个数,如个人双核四线程的计算机就有四个监视器。

▲ 查看CPU和内存的使用率

4.9 线程的生命周期

当线程的数量小于处理器的数量时,线程的并发是真正的并发,不一样的线程运行在不一样的处理器上。

但当线程的数量大于处理器的数量时,线程的并发会受到一些阻碍,此时并非真正的并发,由于此时至少有一个处理器会运行多个线程。

在单个处理器运行多个线程时,并发是一种模拟出来的状态。操做系统采用时间片轮转的方式轮流执行每个线程。如今,几乎全部的现代操做系统采用的都是时间片轮转的抢占式调度方式,如咱们熟悉的Unix、Linux、Windows及macOS等流行的操做系统。

咱们知道线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操做系统中,进程有五种状态,建立、就绪、运行、阻塞(等待)、退出。早期的进程至关于如今的只有单个线程的进程,那么如今的多线程也有五种状态,如今的多线程的生命周期与早期进程的生命周期相似。

▲ 早期进程的生命周期

进程在运行过程有三种状态:就绪、运行、阻塞,建立和退出状态描述的是进程的建立过程和退出过程。

早期进程的生命周期详细说明以下:

建立:进程正在建立,还不能运行。操做系统在建立进程时要进行的工做包括分配和创建进程控制块表项、创建资源表格并分配资源、加载程序并创建地址空间;
就绪:时间片已用完,此线程被强制暂停,等待下一个属于它的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待状态,等待某一事件(如IO或另外一个线程)执行完;
退出:进程已结束,因此也称结束状态,释放操做系统分配的资源。

▲ 线程的生命周期

线程的生命周期跟进程很相似:

建立:一个新的线程被建立,等待该线程被调用执行;
就绪:时间片已用完,此线程被强制暂停,等待下一个属于它的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待状态,等待某一事件(如IO或另外一个线程)执行完;
退出:一个线程完成任务或者其余终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。

五、什么是协程?

5.1 基本常识

协程:英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员本身写程序来管理的轻量级线程叫作“用户空间线程”,具备对内核来讲不可见的特性。

由于是自主开辟的异步任务,因此不少人也更喜欢叫它们纤程(Fiber),或者绿色线程(GreenThread)。

正如一个进程能够拥有多个线程同样,一个线程也能够拥有多个协程。

5.2 协程的目的

对于Java程序员来讲,在传统的J2EE系统中都是基于每一个请求占用一个线程去完成完整的业务逻辑(包括事务)。因此系统的吞吐能力取决于每一个线程的操做耗时。

若是遇到很耗时的I/O行为,则整个系统的吞吐马上降低,由于这个时候线程一直处于阻塞状态,若是线程不少的时候,会存在不少线程处于空闲状态(等待该线程执行完才能执行),形成了资源应用不完全。

最多见的例子就是JDBC(它是同步阻塞的),这也是为何不少人都说数据库是瓶颈的缘由。这里的耗时实际上是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去作运算,而是处于空转状态。而另外过多的线程,也会带来更多的ContextSwitch开销。

对于上述问题:现阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其表明派是 node.js 以及 Java 里的新秀 Vert.x 。

而协程的目的就是当出现长时间的I/O操做时,经过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销。

5.3 协程的特色

协程的特色总结一下就是:

  • 1)线程的切换由操做系统负责调度,协程由用户本身进行调度,所以减小了上下文切换,提升了效率;
  • 2)线程的默认Stack大小是1M,而协程更轻量,接近1K。所以能够在相同的内存中开启更多的协程;
  • 3)因为在同一个线程上,所以能够避免竞争关系而使用锁;
  • 4)适用于被阻塞的,且须要大量并发的场景。但不适用于大量计算的多线程,遇到此种状况,更好实用线程去解决。

5.4 协程的原理

当出现IO阻塞的时候,由协程的调度器进行调度,经过将数据流马上yield掉(主动让出),而且记录当前栈上的数据,阻塞完后马上再经过线程恢复栈,并把阻塞的结果放到这个线程上去跑。

这样看上去好像跟写同步代码没有任何差异,这整个流程能够称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。好比:Golang里的 go关键字其实就是负责开启一个Fiber,让func逻辑跑在上面。

因为协程的暂停彻底由程序控制,发生在用户态上;而线程的阻塞状态是由操做系统内核来进行切换,发生在内核态上。

所以:协程的开销远远小于线程的开销,也就没有了ContextSwitch上的开销。

5.5 协程和线程的比较

六、总结一下

针对上面的内容,咱们总结一下。

进程和线程的区别就是:

  • 1)调度:线程做为调度和分配的基本单位,进程做为拥有资源的基本单位;
  • 2)并发性:不只进程之间能够并发执行,同一个进程的多个线程之间也可并发执行;
  • 3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但能够访问隶属于进程的资源;
  • 4)系统开销:在建立或撤消进程时,因为系统都要为之分配和回收资源,致使系统的开销明显大于建立或撤消线程时的开销。

进程和线程之间的联系就是:

  • 1)一个线程只能属于一个进程,而一个进程能够有多个线程,但至少有一个线程;
  • 2)资源分配给进程,同一进程的全部线程共享该进程的全部资源;
  • 3)处理机分给线程,即真正在处理机上运行的是线程;
  • 4)线程在执行过程当中,须要协做同步。不一样进程的线程间要利用消息通讯的办法实现同步。

那么,既然有了线程,为啥还要有协程?

考虑这同样一种场景:

开发者在每一个线程中只作很是轻量的操做,好比访问一个极小的文件,下载一张极小的图片,加载一段极小的文本等。可是,这样”轻量的操做“的量却很是多。

在有大量这样的轻量操做的场景下,即便能够经过使用线程池来避免建立与销毁的开销,可是线程切换的开销也会很是大,甚至于接近操做自己的开销。

对于这些场景,就很是须要一种能够减小这些开销的方式。因而,协程就应景而出,很是适合这样的场景。

七、参考资料

[1] 进程、线程和协程之间的区别和联系

[2] 一篇文章理解进程、线程、协程

[3] 多进程、多线程与协程的引入

[4] 深刻计算机底层,理解线程与线程池

[5] 深刻操做系统,理解高并发中的协程

附录:更多高性能、高并发文章精选

高性能网络编程(一):单台服务器并发TCP链接数到底能够有多少
高性能网络编程(二):上一个10年,著名的C10K并发链接问题
高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了
高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索
高性能网络编程(五):一文读懂高性能网络编程中的I/O模型
高性能网络编程(六):一文读懂高性能网络编程中的线程模型
高性能网络编程(七):到底什么是高并发?一文即懂!
以网游服务端的网络接入层设计为例,理解实时通讯的技术挑战
知乎技术分享:知乎千万级并发的高性能长链接网关技术实践
淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路
一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
一套原创分布式即时通信(IM)系统理论架构方案
微信后台基于时间序的海量数据冷热分级架构设计实践
微信技术总监谈架构:微信之道——大道至简(演讲全文)
如何解读《微信技术总监谈架构:微信之道——大道至简》
快速裂变:见证微信强大后台架构从0到1的演进历程(一)
17年的实践:腾讯海量产品的技术方法论
腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面
以微博类应用场景为例,总结海量社交系统的架构设计步骤
新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践
重新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

本文已同步发布于“即时通信技术圈”公众号。

▲ 本文在公众号上的连接是:点此进入。同步发布连接是:http://www.52im.net/thread-3357-1-1.html