@kuailezhishang
2015-07-08T17:05:49.000000Z
字数 3335
阅读 4058
面试
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua
)中得到广泛应用。
子程序,或者称为函数,在所有语言中都是层级调用,比如A
调用B
,B
在执行过程中又调用了C
,C
执行完毕返回,B
执行完毕返回,最后是A
执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
看起来A
、B
的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU
呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
因为协程虽然能运行很多函数,但是使用的仍然是一个cpu
核。
没有啥复杂的东西,考虑清楚需求,就可以很自然的衍生出这些解决方案。
一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑。特别是UI
什么的,别一上计算量比较大的玩意就跟死机一样。于是就有了并发,从程序员的角度可以看成是多个独立的逻辑流。内部可以是多cpu
并行,也可以是单cpu
时间分片,能快速的切换逻辑流,看起来像是大家一块跑的就行。
但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个cpu
里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立的程序运行、切换。
后来一电脑上有了好几个cpu
,好咧,大家都别闲着,一人跑一进程。就是所谓的并行。
因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统分分钟被搞跪。所以核心的操作需要陷入内核(kernel
),切换到操作系统,让老大帮你来做。
有的时候碰着I/O
访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU
切换到其他进程,让人家先用着。当然除了I\O
阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我(当前进程)还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB
(虚拟地址到物理地址转换速度的缓存, 快表),只要把寄存器刷新一遍就行,能比切换进程开销少点。
行,故事讲到这里,我们来看看并发的好处:
该干嘛干嘛,多逻辑流先天符合人类描述问题的习惯;
对于I/O
密集的应用,被I/O
阻塞的时候能切换到计算中去,可以显著的提升程序的响应速度。
我们看到阻塞、切换到其他进程(线程)的操作,都会交由操作系统来完成。所以不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call
),先让CPU
跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。
如果我们不要这些功能了,我自己在进程里面写一个逻辑流调度的东西,碰着i\o
我就用非阻塞式的。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是协程。
本质上协程就是用户空间下的线程。
在用户态为每个协程维护调用上下文,以前都是内核帮你维护了,现在要用户态自己来,好处呢就是自己可以控制协程的状态,start
, stop
, 切换到另外一个协程
协程的好处:
跨平台
跨体系架构
无需线程上下文切换的开销
无需原子操作锁定及同步的开销
协程通常是纯软件实现的多任务,与CPU
和操作系统通常没有关系,所以没有理论上限。
唯一的缺点似乎就是:它不能同时将 CPU
的多个核用上。但对 lua
来说这通常不是问题,因为一个宿主程序里面是可以允许有多个 lua
状态机的,开多个线程或进程,然后每个核开一个 lua
状态机即可。
我觉得线程是很丑陋的东西。线程不过是反映了当前硬件技术的物理限制瓶颈。单个cpu
的计算能力不足,所以要多核。内存的容量太小太昂贵,所以需要硬盘。无须敬畏,当你认识到线程不过是个妥协的产物,学习的难度就低多了。比如计算能力低引入了多核,多核引入了并发,并发引入了竞态,竞态引入了锁,一层又一层的引入了复杂性,我等程序员的饭碗才能保住。当然有些问题确实不是单纯的计算能力或存储能力极大提升就能解决的,不是我的工作范围,就不献丑了。
协程比线程更基础。协程不能像线程那样,简单看做一种硬件妥协机制。协程是可以作为语言的内建机制的,因为协程反映了程序逻辑的一种需求:可重入能力。这个能力很重要,因为大多数语言的一个最重要的组件--函数,其实就依赖这个能力的弱化版本。函数中的局部变量,被你初始化为特定的值,每次你调函数,换种说法:重入函数,语言都保证这些局部变量的值不会改变。相同的输入,得到相同的输出。当然你跟我扯全局变量就没意思了。
语言实现到函数这一步,可以满足绝大多数日常需求了。但工程师就是又懒又爱折腾啊。函数在很多语言特别是早期语言中,有个别名:过程(具体特性不一定相同,就不追究了,整体的行为还是差不离的)。我觉得过程这个词比函数更贴切。现在我们把“函数中局部变量的值”换种说法,叫做“过程中的局部状态”,这个说法更广泛了。每次重入过程,过程中的局部状态都被重置。要想得到不同的输出状态,唯有改变输入的状态。要想明确一个输出状态,对应的输入状态,唯有记录下输入状态。so simple,so native
。问题是那帮懒惰的工程师甚至连输入状态都不想保存判断啊。他们希望有这么一种过程,每次进入,过程里的局部状态,都能保留上一次进入的状态。自然也就不需要干针对输入状态的保存或判断工作了。换言之,这个特殊过程把原来需要在过程之外的用来控制过程输出状态的那些输入状态的管理工作,都交给过程本身了。
这个特殊的过程,就叫做协程。它能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。普通过程(函数)可看成这个特殊过程的一个特例:只有一个状态,每次进入时局部状态重置。这种逻辑控制上的方便当然让这帮懒惰的工程师乐翻了天,少打了好多字,可以向老板叫嚣生产力提高了,其实又可以多lol几把了,对不对?用协程的好处是,你处在更高的逻辑层面去审视需求,整理代码。没有函数之前,你只有表达式,没有表达式之前,你只有mov
。没有if-else
之前,你只有jump
。脱离低级的逻辑处理,就意味着能在更高的抽象层面看问题。就好像如果你在算傅里叶变换时,还要每次去思考四则混合运算规则,只能是自作死。协程之所以陌生,是因为这个能力很强大,因此通常跟实际业务联系很紧密吧。
因此,协程不过是一个逻辑控制需求。一些语言原生支持,不支持也可以用原有的材料构建出来。协程的实现,无非是你要维护一组局部状态,在重新进入协程前,保证这些状态不被改变,你能顺利定位到之前的位置。你平时所写的一些逻辑控制代码,经典如状态机或对象等,也许就已经是一种“协程”了。区别在于是否精巧,适用条件是否苛刻,使用是否方便,效率是否足够罢了。
面向对象中的对象,函数式语言中过程的chunk
实现,都跟协程有些相似的结构。这些语言的表达足够丰富,有没有协程,倒不构成问题。真要说起来,我觉得协程的最大的好处是在写过程式(命令式)风格的代码时,很好的简化了逻辑的流程。