协程

来源:互联网 发布:windows 2008 加入域 编辑:程序博客网 时间:2024/06/05 19:15

我心中的协程

最近在研究网络服务框架方面的东西,发现了一个神奇的东西-协程。
一句话说明什么是线程:协程是一种用户态的轻量级线程

一句话并不能完全概括协程的全部,但是起码能让我们对协程这个概念有一个基本的印象。
从硬件发展来看,从最初的单核单CPU,到单核多CPU,多核多CPU,似乎已经到了极限了,但是单核CPU性能却还在不断提升。server端也在不断的发展变化。如果将程序分为IO密集型应用和CPU密集型应用,二者的server的发展如下:
IO密集型应用: 多进程->多线程->事件驱动->协程
CPU密集型应用:多进程-->多线程

如果说多进程对于多CPU,多线程对应多核CPU,那么事件驱动和协程则是在充分挖掘不断提高性能的单核CPU的潜力。
以下的讨论如无特别说明,不考虑cpu密集型应用。

异步 vs 同步

无论是线程还是进程,使用的都是同步进制,当发生阻塞时,性能会大幅度降低,无法充分利用CPU潜力,浪费硬件投资,更重要造成软件模块的铁板化,紧耦合,无法切割,不利于日后扩展和变化。不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。多个线程之间在一些访问互斥的代码时还需要加上锁,这也是导致多线程编程难的原因之一。

现下流行的异步server都是基于事件驱动的(如nginx)。事件驱动简化了编程模型,很好地解决了多线程难于编程,难于调试的问题。异步事件驱动模型中,把会导致阻塞的操作转化为一个异步操作,主线程负责发起这个异步操作,并处理这个异步操作的结果。由于所有阻塞的操作都转化为异步操作,理论上主线程的大部分时间都是在处理实际的计算任务,少了多线程的调度时间,所以这种模型的性能通常会比较好。

总的说来,当单核cpu性能提升,cpu不在成为性能瓶颈时,采用异步server能够简化编程模型,也能提高IO密集型应用的性能。

协程 vs 线程

之前说道,协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务

不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。
而且由于抢占式调度执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题(事件驱动和异步程序也有同样的优点)。

我们在自己在进程里面完成逻辑流调度,碰着i\o我就用非阻塞式的。那么我们即可以利用到异步优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是协程。

协程 vs 事件驱动

以nginx为代表的事件驱动的异步server正在横扫天下,那么事件驱动模型会是server端模型的终点吗?
我们可以深入了解下,事件驱动编程的模型。
事件驱动编程的架构是预先设计一个事件循环,这个事件循环程序不断地检查目前要处理的信息,根据要处理的信息运行一个触发函数。其中这个外部信息可能来自一个目录夹中的文件,可能来自键盘或鼠标的动作,或者是一个时间事件。这个触发函数,可以是系统默认的也可以是用户注册的回调函数。

事件驱动程序设计着重于弹性以及异步化上面。许多GUI框架(如windows的MFC,Android的GUI框架),Zookeeper的Watcher等都使用了事件驱动机制。未来还会有其他的基于事件驱动的作品出现。

基于事件驱动的编程是单线程思维,其特点是异步+回调。
协程也是单线程,但是它能让原来要使用异步+回调方式写的非人类代码,可以用看似同步的方式写出来。它是实现推拉互动的所谓非抢占式协作的关键。

总结

协程的好处:

  • 跨平台
  • 跨体系架构
  • 无需线程上下文切换的开销
  • 无需原子操作锁定及同步的开销
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序:这一点和事件驱动一样,可以使用异步IO操作来解决



我觉得线程是很丑陋的东西。线程不过是反映了当前硬件技术的物理限制瓶颈。单个cpu的计算能力不足,所以要多核。内存的容量太小太昂贵,所以需要硬盘。无须敬畏,当你认识到线程不过是个妥协的产物,学习的难度就低多了。比如计算能力低引入了多核,多核引入了并发,并发引入了竞态,竞态引入了锁,一层又一层的引入了复杂性,我等程序员的饭碗才能保住。当然有些问题确实不是单纯的计算能力或存储能力极大提升就能解决的,不是我的工作范围,就不献丑了。

协程比线程更基础。协程不能像线程那样,简单看做一种硬件妥协机制。协程是可以作为语言的内建机制的,因为协程反映了程序逻辑的一种需求:可重入能力。这个能力很重要,因为大多数语言的一个最重要的组件--函数,其实就依赖这个能力的弱化版本。函数中的局部变量,被你初始化为特定的值,每次你调函数,换种说法:重入函数,语言都保证这些局部变量的值不会改变。相同的输入,得到相同的输出。当然你跟我扯全局变量就没意思了。

语言实现到函数这一步,可以满足绝大多数日常需求了。但工程师就是又懒又爱折腾啊。函数在很多语言特别是早期语言中,有个别名:过程(具体特性不一定相同,就不追究了,整体的行为还是差不离的)。我觉得过程这个词比函数更贴切。现在我们把“函数中局部变量的值”换种说法,叫做“过程中的局部状态”,这个说法更广泛了。每次重入过程,过程中的局部状态都被重置。要想得到不同的输出状态,唯有改变输入的状态。要想明确一个输出状态,对应的输入状态,唯有记录下输入状态。so simple,so native。问题是那帮懒惰的工程师甚至连输入状态都不想保存判断啊。他们希望有这么一种过程,每次进入,过程里的局部状态,都能保留上一次进入的状态。自然也就不需要干针对输入状态的保存或判断工作了。换言之,这个特殊过程把原来需要在过程之外的用来控制过程输出状态的那些输入状态的管理工作,都交给过程本身了。

这个特殊的过程,就叫做协程。它能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。普通过程(函数)可看成这个特殊过程的一个特例:只有一个状态,每次进入时局部状态重置。这种逻辑控制上的方便当然让这帮懒惰的工程师乐翻了天,少打了好多字,可以向老板叫嚣生产力提高了,其实又可以多lol几把了,对不对?用协程的好处是,你处在更高的逻辑层面去审视需求,整理代码。没有函数之前,你只有表达式,没有表达式之前,你只有mov。没有if-else之前,你只有jump。脱离低级的逻辑处理,就意味着能在更高的抽象层面看问题。就好像如果你在算傅里叶变换时,还要每次去思考四则混合运算规则,只能是自作死。协程之所以陌生,是因为这个能力很强大,因此通常跟实际业务联系很紧密吧。

因此,协程不过是一个逻辑控制需求。一些语言原生支持,不支持也可以用原有的材料构建出来。协程的实现,无非是你要维护一组局部状态,在重新进入协程前,保证这些状态不被改变,你能顺利定位到之前的位置。你平时所写的一些逻辑控制代码,经典如状态机或对象等,也许就已经是一种“协程”了。区别在于是否精巧,适用条件是否苛刻,使用是否方便,效率是否足够罢了。

面向对象中的对象,函数式语言中过程的chunk实现,都跟协程有些相似的结构。这些语言的表达足够丰富,有没有协程,倒不构成问题。真要说起来,我觉得协程的最大的好处是在写过程式(命令式)风格的代码时,很好的简化了逻辑的流程。


0 0
原创粉丝点击