跳过正文

说清楚!到底什么是协程

·70 字·1 分钟
Remy
作者
Remy
Bug的设计师,故障的制造机,P0的背锅侠。代码里的隐秘问题总能被我创造性地解锁。写代码如同解谜,有时谜底是惊喜,有时是惊吓。

在程序遇到性能瓶颈时,解决办法有很多种,其中解决方法之一就是采用并发编程技术。像python这样所谓执行“低效”的语言,如何高效的使用并发能力总是被屡屡提起。而经常被别的语言常用的并发技术“线程“因为某些特殊原因(GIL)在python中又不是那么高效,被人经常诟病为:食之无味,弃之可惜.多进程上下文切换太消耗系统资源,多线程又不让好好玩,突然感觉天都是灰蒙蒙的,我python难道真的就这么不堪吗?于是python开发者经常口口相传的协程就被抬到桌面上来了。那么到底是什么协程?有人说用户级别的线程,有人说时绿色线程,轻量级线程。作为一只码狗,看过很多资料后,拨开了迷雾,终于略懂一二。当然如果你看到本文,如果理解有误,可以联系我纠正,才疏学浅也就理解到这个地步了

相关概念
#

进程(Process)
#

  1. 进程其实在最开始时是运行程序的基本单位,如linux 内核2.4版本之前, 在这个时代是真正为了适应多核的处理器而生,如一边打游戏,一边听音乐,让两个任务能够真正的并行,也可能只是分时复用,进程的基本模型和基本行为,都是由操作系统定义的,编程语言只能遵照实现。
  2. 随后技术的进步,目前的操作系统都是使用的面向线程的模型,如linux 2.6内核后。操作系统调度的任务的单位是线程,进程变成只是用于隔离不同进程,不同的进程间资源不共享,进程内资源可共享。
  3. 当然,有时候,有些编程语言,故意把概念稍微改变一下,就令人十分难以理解,比如erlang的进程(process)就完全跟系统的进程概念完全不同,让人十分费解,这是由于,虽然操作系统提供了对进程和线程的接口定义权,但是具体实现依然由语言来做,erlang只是把单方面把操作系统的线程定义为了process罢了。

子进程,父进程,主进程
#

  • 可以从一个进程中启动别的进程,新启动的称为子进程,启动子进程的进程为父进程,原始最先执行的进程称为主进程。

线程(Thread)
#

  • 上文说过,线程事现代操作系统调度任务的基本单位,是进程的组成部分。同一个进程下可以有很多线程,可以共享该进程拥有的计算资源,各个线程之间所拥有的资源一般不共享。

  • 操作系统使用线程模型,是为了提供任务分解为多个子任务并发或者并行运行的解决方案,以提高程序的执行效率,比如你正在shell上疯狂的敲打着代码,操作系统既要监听键盘的操作,又要渲染你打出的代码,还要跟远程服务器进行通信,这些都是拆分成独立的任务拆分到不同的线程去执行,多线程可以真正的利用多核CPU并行执行,亦可分时服用,线程的接口依然由操作系统定义,编程语言实现。

  • 等等!!!!为啥操作系统的线程可以并行执行任务,而python的线程就这么蠢呢?一样,python实现的多线程被称为协作式线程,为了让程序猿少掉头发,引入了GIL的概念,使得python在一个进程内的多线程只能分时复用轮流执行。这个既让人痛恨,又让人不舍得丢弃。真可谓是食之无味,弃之可惜。好处是大大降低了了编写并发程序的难度,坏处也是十分痛恨,不能充分利用多核优势。

    例程
    #

    最简单理解就是一个函数或者一个方法,直到return结束,专业点就叫做可被调用的代码块

并行,并发,讲清楚,何为协程?
#

并发
#

并发是计算机科学中最难的概念之一,没事不要去招惹这个家伙 - David Beazley

并发

并发指的是程序的组织结构。程序要设计成多个独立执行的子任务,利用有限的资源是多个人物可以被实时或者近实时执行的目的。简单的说就是一个程序干很多事。就像你一边在银行排队,一边跟你女票扯家常,手里还能把玩着拇指陀螺,你在这一刻,你一个人干着3个任务。

并行
#

并行指的是程序执行的状态,多个人任务在多个逻辑处理器上执行,利用富余的计算资源加速完成多个任务的目的

并行

协程(co-routine)
#

协程的概念其实比线程更早,在处理多任务的设计上,由于线程在工程上更加容易,使得协程就在此搁置了。那么到底协程难在哪里?我们一点一点来看

协程是非抢占式的多任务子例程的概括,可以润恤多个入口点在例程中确定位置来控制程序的暂停和恢复执行。多个入口点是指可以在一个协程内多次使用如同yield的关键字,比如我们写以下程序:

def coroutine():
    yield "1"
    yield "2"
    yield "3"

c = coroutine()
c.next() # 1

这时,我们程序就挂起,程序控制权即被交出去,可以执行其他程序,当我们继续调用next c.next则为2 再次调用next则为3,此时yield后程序就将程序控制权交出,可以执行其他代码,然后翻回来继续执行。每个yield的位置,都是程需要可以使之让出执行权,暂停,恢复,传递信号,注入执行结果等等。

高德纳说,例程是协程的特例。协程 例程本质上时一回事,不过表现有所差异,例程怎么定义,协程一样怎么定义。

函数在线程内执行,协程也是如此,多个协程共享着线程的拥有的资源。由于协程就是例程,在线程内初始化,,协程的数据结构存放在线程栈内存中。所以,协程的切换实际上就是函数的调用,在栈内存中完成。所以,我们常说,协程消耗资源很少,我们可以开很多都可以。协程的切换要比线程和进程小太多,消耗的系统资源一样比小很多。所以提高了整个系统的利用率。协程还可以跨线程调度,就像函数可以放到另一个线程去执行时一样的。

那么协程难在哪呢?协程拥有多个入口点,每yield的一次,接下来调度谁,也就是yield谁,运行什么任务,需要多少资源,谁先执行,谁后执行,怎么执行,都是由程序员来做。而线程和进程不同,线程和进程都是由操作系统来调度,而且是抢占试调度,同级别的线程,优先级,什么时候返回,什么时候暂停,执行顺序都是不可预知的,而协程则可以被安排。如此,如此写好调度协程直接影响了整体程序的质量,这对工业和工程来说是致命的。本来被包办的东西,所有东西变得亲力亲为。所以程序员的质量直接影响了整个程序的质量。是啊,被照顾的太舒服的东西总不是那么高效的。

协程和一般例程的区别在于,函数执行是从第一行开始,直到返回结束,结束了就是结束了,生命到此终结了,函数在各次调用见,不会保存之前的执行状态,而协程的暂时退出,是靠调用别的协程实现的。协程的调用,还会保存之前的执行状态,切换到另一个协程后,翻回来继续执行。协程执行的起点,是进入该写成的入口点,不一定是协程定义的第一行代码,该次调用的终点,也不一定是协程的最后一行代码。

杂谈
#

其实协程概念远远早于线程,只不过有些语言的实现要么换了点概念,比如erlang,有的提供了更好的API。毕竟天底下没有新鲜事,上就是协程,只不过封装了更好的api,调用更加方便,比如js,为了解决回调地狱,先试上了promise,随后引入生成器有了协程的模型,随后马上推出了 async,await的语法糖,当然python3.6也反过来吸收了该语法。更如golang将协程的调度直接写入语法底层,能够利用多核的能力(前面说了,协程可以跨线程调用,当然golang的实现更加复杂,有更加厚的调度层。但是原理基本如此)。

协程是用户态线程?纯属胡说八道!
#

黑人问号
刚开始我也这么认为,后来发现徒增自己理解的难度,原因如下

  1. 操作系统在执行代码的时候,会对代码区别对待,使代码具有不同的权限,为了防止程序有诸如操作不同内存区域,对各种计算机资源的访问的权限,实现保护模式。避免程序员提供给操作系统的代码影响系统稳定性,当然,比如病毒,所以操作系统内核的代码几乎拥有所有特权,所谓的ring0级别内核态。
  2. 我们这种写面向一般用户的,基本是ring3级别一般权限,所以只要你没入侵内核态,你的代码就是用户级别的,不管是线程还是进程还是协程都是如此。就算是病毒帮你下毛片了,下毛片的代码依旧是用户态代码。
  3. 不要混淆以上概念,加重理解负担,要严谨不要虾扯蛋。