Linux【9】-进程管理1-1--进程,线程,协程

理解这几个概念前可以先看如下两篇博文,通俗易懂。

一、前言

现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。

什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?

答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

对于操作系统来说,一个任务就是一个进程(Process) ,比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)

由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

我们前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?

有两种解决方案:

  • 多进程模式: 一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
  • 多线程模式: 还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
  • 多进程+多线程模式: 当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

同时执行多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,有时,任务3和任务4又不能同时执行,所以,多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。

因为复杂度高,调试困难,所以,不是迫不得已,我们也不想编写多任务。但是,有很多时候,没有多任务还真不行。想想在电脑上看电影,就必须由一个线程播放视频,另一个线程播放音频,否则,单线程实现的话就只能先把视频播放完再播放音频,或者先把音频播放完再播放视频,这显然是不行的。

并发与并行

并发描述的是一个程序是否被分成了多个可执行片段,使得每个片段都可以独立运行,不同执行片段通过通信来进行协调。

首先应该明确的一点就是并发和并行的概念.并发是程序级的概念,描述的是一段程序里面可执行片段的多少,所以在程序中使用进程,线程,协程都能提高程序的并发性。而并行描述的是程序的计算过程,在同一时间内,该程序同时执行多个可执行片段。现代操作系统中的最小可执行单位是线程,你创建一个进程,操作系统实际执行单位也是该进程中的线程,也就是说只要你的计算机CPU是有多核的,而且你的程序被分成了多个线程或者进程,那每个CPU都会执行其中的一个线程 (没别的任务的情况下)这时候程序就是并行的。而协程只是一个线程中的一部分执行片段,所以单纯的使用协程并不能提高程序的并行性,只能提高单核CPU的并发性。下图表明在双核CPU的计算机下多线程和多协程的运行情况(python中因为GIL的存在是无法做到多线程的,图中只是理想情况下-。-)。

多线程多协程代码的执行情况

二、基本概念

1、进程(process)

狭义定义:进程就是一段程序的执行过程。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

简单的来讲进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。

进程状态:进程有三个状态,就绪、运行和阻塞。就绪状态其实就是获取了出cpu外的所有资源,只要处理器分配资源就可以马上执行。就绪状态有排队序列什么的,排队原则不再赘述。运行态就是获得了处理器分配的资源,程序开始执行。阻塞态,当程序条件不够时候,需要等待条件满足时候才能执行,如等待i/o操作时候,此刻的状态就叫阻塞态。

2、程序

说起进程,就不得不说下程序。先看定义:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是在处理机上的一次执行过程,它是一个动态的概念。这个不难理解,其实进程是包含程序的,进程的执行离不开程序,进程中的文本区域就是代码区,也就是程序。

3、线程

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

4、多线程

在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也不可能只有一节车厢。多线程的出现就是为了提高效率。

5. 协程

协程的应用场景

要了解协程,首先要知道当cpu处理系统io操作时,会等待这些操作的完成,你可知道这短短的等待时间cpu可以执行多少命令,下面是node.js的创始人在一次演讲上展示的图。

现在的电脑从不同的储存设备中读取数据的延迟(最后一列只是类比)

当今的计算机cpu普遍性能可以达到2000000000次/秒,简单计算下从硬盘中取数据会让cpu什么都不干0.0025秒,那段时间本来可以执行5000000条指令啊-_-。于是就诞生了两种方法来防止io的阻塞。

  1. 为每个会阻塞的操作单独创建一个线程,这样你取你的,我运行我的,就可以提高cpu的利用率
  2. 把每个会阻塞的操作变成一个异步调用,我先跑我的,等你取完了通知我再收拾你

线程的切换有个问题,切换线程消耗的资源多,还有锁。其实最重要的是python因为gil的存在,对线程的支持十分有限。 在第二种方法中可以分为异步回调和协程两种方式。这两者最大的区别是协程在内存中维护自身的运行状态和上下文,这样程序的逻辑流表现起来就非常简单,而异步回调则是通过在调用函数中注册回调函数,调用函数的运行状态通过回调函数返回。所以一般来说回调函数的实现都会比协程复杂(关于回调具体可以看 http://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/

python中的协程:

在python中协程的实现是通过yield来实现的,有些同学可能知道yield是用来把函数变成生成器的却不知道它也可以用于协程。其实仔细一想你就知道,yield命令其实会保存当前函数的上下文,然后返回一个数值给调用它的函数。再想想上面的协程实现,嘿嘿,懂了吧。

三、区别

进程的执行过程是线状的, 尽管中间会发生中断或暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是要被保护起来的。这是进程宏观上的执行过 程。而进程又可有单线程进程与多线程进程两种。我们知道,进程有 一个进程控制块 PCB ,相关程序段 和 该程序段对其进行操作的数据结构集 这三部分,单线程进程的执行过程在宏观上是线性的,微观上也只有单一的执行过程;而多线程进程在宏观上的执行过程同样为线性的,但微观上却可以有多个执行 操作(线程),如不同代码片段以及相关的数据结构集。线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。除了 CPU 之外,计算机内的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。与进程控制表和 PCB 相似,每个线程也有自己的线程控制表 TCB ,而这个 TCB 中所保存的线程状态信息则要比 PCB 表少得多,这些信息主要是相关指针用堆栈(系统栈和用户栈),寄存器中的状态数据。进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。

进程是资源分配的基本单位,线程是调度的基本单位。进程的个体间是完全独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其他进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。而任何一个子线程终止一般不会影响其他线程,除非子线程执行了exit()系统调用。任何一个子线程执行exit(),全部线程同时灭亡。

从系统实现角度讲,进程的实现是调用fork系统调用;线程的实现是调用clone系统调用。fork()是将父进程的全部资源复制给了子进程。而线程的clone只是复制了一小部分必要的资源。在调用clone时可以通过参数控制要复制的对象。

其实在Linux内核2.4版以前,线程的实现和管理方式就是完全按照进程方式实现的。在2.6版内核以后才有了单独的线程实现。

四、小结

线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。

对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。

参考资料

药企,独角兽,苏州。团队长期招人,感兴趣的都可以发邮件聊聊:tiehan@sina.cn
个人公众号,比较懒,很少更新,可以在上面提问题,如果回复不及时,可发邮件给我: tiehan@sina.cn