进程、线程、协程、例程、过程的区别是什么?


引自我在知乎上的回答:进程 线程 协程 例程 过程 的区别是什么? - 骏马金龙的回答 - 知乎

首先解释下程序、进程、上下文切换和线程。然后再解释协程、例程、过程。

程序

程序:源代码堆起来的东西。相当于一个一动不动没有生命的机器人。

  • 虽然是没有生命的机器人,但是它被设计后就表示有了硬件,它的硬件决定了之后它有生命后是如何干活的
  • 机器人有优劣,所以有些优秀的机器人干活很快,而有些机器人干活很慢

进程

进程:程序在系统上跑起来(运行)之后的东西(动态的)。相当于有了生命的机器人。生命是内核给的,动起来的能力是CPU提供的驱动力。

  • 因为在操作系统看来,它已经有了生命,会赋予它一些属性,比如它叫什么(PID),谁发明的(PPID),它在哪个范围内活动(地址空间).................
  • 内核会记录这个机器人的信息
  • 机器人可以造机器人(父子进程)
  • 它可以中断或睡眠。比如机器人想充电,但是没电给它,它得等
  • 内核可以跟它交流,比如传递一些数据给它、传递信号给它
  • 它可以被毁掉。比如进程崩溃或正常退出或被信号终止
  • 机器人毁掉后要为它收尸,如果机器人偷偷死掉,就会没人给它收尸而变成僵尸进程
  • 严格地说,这个机器人运行起来之后虽然有了生命,但是没有灵魂,只有CPU给它驱动力的那一段时间,他才能动起来,其它时候都是在哪里睡觉
  • .......

上下文切换

上下文切换:在某一时刻,一核CPU只能执行一个进程。相当于内核只能让一核CPU为一个机器人提供驱动力让它动起来(1:1),多核CPU可以同时为多个机器人提供驱动力。

  • 但是操作系统上有很多机器人在干活,所以内核要控制CPU不断的为不同机器人来回提供驱动力,这是进程切换(这是站在内核的角度上看的,也叫上下文切换)
  • 为了让你感觉机器人没有停止工作,内核控制只给每个机器人一点点的CPU时间片。这就相当于转起来的电扇,你感觉没有间隙,其实是有的,只是间隙时间太短,人眼难辨。现在可以脑部一下,一大堆的机器人在你面前完全不停的跳着鬼步舞....
  • 因为CPU要切换给不同的机器人提供驱动力,所以每次切换之前的机器人干活到了哪里以及它的状态得记录下来,这是上下文保留(保护现场)
  • 保护现场是必要的额外的工作,频繁上下文切换会浪费CPU在这些额外工作上
  • .........

线程

线程:一个进程内可以有多个执行线程,或者说线程是进程内部的小型进程。相当于在机器人内部根据机器人自身克隆了很多个基本完全相同的体内小机器人。

  • 进程内部可以有多个线程同时执行
  • 进程内的所有线程拥有的源代码完全相同。只是有些小型机器人执行任务A,有些小型机器人执行任务B。而这些任务原本是应该被那个大机器人完成的
  • 所有小型机器人活动范围相同,即某进程内所有线程都共享地址空间。但是每个小型机器人也得有自己的一点私人空间(线程有自己的栈空间)
  • 小型机器人共享很多属性(都来源于大机器人),也有很多自己的属性
  • 线程也要切换。只是线程切换需要做的额外工作要比进程切换少的多

现在,对比一下多进程和多线程?

函数

然后是协程、例程、过程。但是在解释这个之前,先解释下现在更为俗知的函数,以及它们和进程、线程之间的关系。

函数:一种代码段。用来表示一个要完成的任务单元。当然,这个任务里可能也包含了其它多个子任务。

再说程序

  • 程序的主体部分是函数,包括一个程序的入口函数(main函数,有些语言(一半是动态语言)不要求你敲main函数的声明,这些语言会在程序执行的时候默默给你加上main)以及其它一些自己编写的函数
  • 除了函数外,程序中可能还有一些全局属性的定义、一些额外的属于编程语言自身的额外代码,比如说程序文件头部可能声明这是一个包以及要导入什么包之类的
  • 函数是要被进程(或线程)执行的,机器人干什么的?就是执行函数的。程序的运行从执行入口函数main开始,然后在main中调用其它你自己编写的函数,也就是说跳转到其它函数上去执行一个个的任务。画重点,函数是要被执行的,函数的执行是可以进行跳转的
  • 那些非函数代码(比如全局属性的定义代码、导包代码),是在程序被装载准备执行的时候完成好的工作。

先总结一下重点:进程、线程是机器人,函数是机器人要干的活

函数怎么执行的

机器人是怎么执行函数的呢?在不使用coroutine(也就是国人翻译后的"协程")的情况下,它的执行流程是固定的:从main函数开始,跳转执行其它函数A,main函数被搁置等待,它必须等待跳转后的函数A执行完返回后才能回到main函数继续向下执行,如果函数A中还有调用函数B,那么函数A被搁置等待,它必须等待函数B执行完返回后才继续向下执行。直到main函数也执行完,程序退出。

继续用机器人来比喻,那就是机器人要执行一个任务,但这个任务中要求临时去执行另一个任务,那么这个机器人必须先去执行另一个任务,并只能在执行完另一个任务的时候回到之前的那个任务继续执行。

所以,重点是:函数A中在第X行开始跳转调用函数B时,函数A必须等待函数B执行完返回后才能继续从第X行处开始继续向下

协程、例程、过程

回到正题要解释的东西:协程、例程、过程。

很遗憾,例程、过程、子程序,它们都是函数的不同称呼,不同的时代、不同的编程语言称呼的方式不一样,都是任务单元。甚至面向对象里的方法也是函数,只不过在面向对象的面具之下,它有一些和面向对象相关的特性。

最后是协程。我猜你说的协程应该是coroutine这类东西,全称是cooperative routine,也叫做cooperative tasks。只看英文的话,意思已经很明显了:协同运行的子程序(子程序就是例程、函数、过程)。或者说是协同执行的任务。

在解释coroutine之前,多插一句嘴。
coroutine被翻译为协程,个人认为是不太合理的。在shell中有coproc的概念(ksh/bash等一些shell都支持coproc的功能),cooperative process,表示协同运行的进程,按单词字面意思,这才应该被翻译为协程。

协同运行?这是个什么意思。回顾下刚才解释函数(因为是co routine,所以我下面全部用routine来替换函数的说法)的执行流程在理解协同运行是什么意思。

在正常情况下,routine跳转运行后必须原地等待跳转后的那个routine执行完返回才能继续从原地向下执行。

但是,使用coroutine的时候,假设routine1和coroutine2互为coroutine,那么routine1跳转到routine2去执行的时候,它会等待routine2才能继续向下执行,但是不一定是等待routine2执行完,也可能是等待routine2重新跳转回routine1(因为routine2已经产生了一些可以让routine1继续运行的数据,而不是让routine1继续在那里阻塞)。同理routine2也可能会原地等待routine1,routine1再跳回routine2。

但是这怎么行,来来回回的跳转总得退出吧?这就是跟所写的代码有关系了。比如某个routine中加入一个判断,达到某一条件时就不再跳转,而是直接向下执行。

机器人的比喻又来了。机器人要执行一个任务,但要求临时去执行另一个任务,现在不强制规定必须执行完另一个任务才回来执行原始任务,而是可以在执行另一个任务的时候,又临时回来执行原始任务。

如果说不好理解,那么下面这个wiki中给的生产者消费者模型的伪代码很容易帮助理解coroutine,这个程序不一定合理,但非常适合于理解"协同"的意义。其中q是一个队列,生产者coroutine会跳转到消费者coroutine,同理消费者coroutine也一样会跳转到生产者coroutine。

var q := new queue

coroutine produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield to consume

coroutine consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield to produce

这就是协同运行以及coroutine的解释,我想应该已经不难理解了。

coroutine的优点

最后,说一下coroutine的优点:

实际上,coroutine可以认为是单线程多任务的工作方式(当然,进程中实现coroutine也是可以的),因为它在单个线程中的多个任务之间直接跳转,而多线程是通过上下文切换来实现多任务的。换句话说,coroutine提供了并发却不并行的功能。通过coroutine,还能实现更为"实时"的上下文任务,因为coroutine之间的跳转切换不需要任何系统调用和可能的阻塞调用,不需要像多线程一样为了线程之间的资源同步而使用额外的互斥锁、信号量等。

相关