再谈协程

若是你对如下几个问题有疑问,那么本文可能会有所帮助。html

  1. 什么是协程,或者说为何会有协程这个概念?
  2. 怎么用?何时须要用?
  3. 都有并行的意味,那么协程和多线程有什么区别?二者可否相互替代?
  4. 协程底层的实现原理。

1.2.3
linux

谈协程绕不开线程,按传统还得从进程谈起,不过我想业内人员对进程和线程应该是耳熟能详,这里就简单归纳下。git

进程拥有本身独立的堆和栈,既不共享堆,亦不共享栈,进程由操做系统调度;线程拥有本身独立的栈,共享堆(也能够有本身的私有域),不共享栈,线程亦由操做系统调度。一个进程能够有多个线程。github

多线程一直以来是面试必考点,虽然[web]服务端开发人员彷佛历来不用直接操做线程,实际上是由于框架帮忙维护了,开发人员只须要关心业务实现。这也致使了部分人对多线程的某些概念模糊不清。好比关于多线程的效率:在多核cpu下,多个线程能够并行运行在不一样内核上,效率高;而在单核cpu中,多个线程的并行执行实际上是一个错觉,由于它们都是运行在一个内核上,一个cpu内核同一时间只能执行一个进程/线程,所以在一个内核上的多线程执行其实效率反而比串行执行低,只是给用户一种并发的错觉,反而增长了线程切换的时间。web

可是效率的高低还要看线程占用cpu资源的占用率,好比存在大量IO操做,IO比较慢。也就是说,若是只有单线程,那么一旦涉及到IO操做,线程可能会被阻塞,程序的其他逻辑就只能傻等,就算那些逻辑不依赖于这个IO操做,此时线程对CPU的使用为0,CPU就是空闲状态。若是是多线程,是线程瓶颈,那么其他线程则可使用cpu,而非等待IO结束。面试

题外话,一个空循环就能让cpu满载,参看 为何一个空的死循环会让CPU占用达到100%算法

后来,出现了多路复用之类的技术,原先须要等待IO返回的线程也不须要等了,能够和其它线程同样忙别的事,IO返回时获得通知再处理接下去的事情。Java的NIO和.Net的async/await就是这么干的。编程

通常来讲,为了不线程频繁建立销毁带来的性能问题,程序里都会使用到线程池。windows

然而仍是在单核的场景下,事情彷佛变得有点诡异。既然线程们如今都能心无旁骛地使用CPU计算,而前面也说了,一个cpu内核同时只能运行一个线程,管理多线程又是抢占式,又是栈切换,维护生命周期啥的,影响性能不说,彻底没得必要嘛,为何不仅用一个线程完成全部的计算呢。什么,你说可能须要[伪]并行计算?那就让线程本身来安排咯,毕竟具体逻辑方面,线程自己(或者说开发人员)比CPU要清楚的多,知道何时该干什么,何时切换逻辑,何时不切换,都由线程本身说了算。因而,协程粉墨登场。api

协程主要是针对单线程的一个概念(如Js、NodeJs、Python因为GIL致使的伪多线程),能够将其看做线程运行时片断。和线程相似,虽然貌似多个协程能够并行执行,一个时间仍然只能运行一个。因此,若是业务逻辑是顺序相关(串行)或者各任务对反馈及时性要求不高,那么不必用协程,就跟不必多线程同样。协程对比线程,除了有更好的性能外,还让开发人员对执行片断有了更好的掌控。好比Go语言,经过阻塞条件(time.sleep()、select{}等),咱们能够手动将控制权转移给其它的 Go 协程 , 也能够说是告诉调度器让它去调度其它可用空闲的 Go 协程(Go如何判断这是阻塞代码还没有研究过);或者经过channel调度指定协程。

Go默认状况下只用单线程。这就是说,你即便开了几百个goroutine,系统中同一时间在跑的只有一个线程,也就是一个协程。依据上面的内容,你们能够思考下Go为什么默认如此。咱们能够经过 runtime.GOMAXPROCS() 设置的是Go语言能跑几个线程,讲道理,CPU几核跑几个线程比较合理,使用 runtime.NumCPU() 查看内核数。

在编程层面来讲,协程的概念偏向于以同步编程的模式实现异步处理的编程模式,避免了多层回调代码嵌套的问题。

其实在不少年之前,协程已经被提出了,如今只是它焕发生机的阶段。


4

上文说了,协程之间应该是非顺序相关的,即它们的上下文没有强依赖关系,是相对独立的。这里的上下文指的就是当前的运行栈空间,它包括了参数、局部变量、各寄存器的值等内容。在协程切换的时候,咱们要想办法将对应的上下文投射到当前线程的运行栈中,即让线程执行特定的上下文。很容易想到malloc一块临时内存存放挂起的协程上下文信息,resume的时候再覆盖回去,运行栈在内存中只有一处,这就是stackless模式。相对的还有stackful模式,在这种模式下,每一个协程都有本身的栈空间,运行栈指的就是当前协程的栈空间。现有语言的实现中,Python, Kotlin等定义的就是stackless协程, Go语言中实现的是stackful协程。

对于其它没有在语言层面直接支持协程的语言来讲,因为协程涉及到底层的[堆]栈切换控制,所以很难单纯依靠现有语法构建算法的方式实现。有人作过此类尝试(可参看Coroutines in C),但也没有实用性。

能直接操做执行堆栈并暴露api的,如今市面上的语言以C/C++最为流行,基于它们也有不少开源的协程库。下面介绍几种实现方式。

协程分为非对称协程和对称协程。在非对称协程中,调用者和被调用者的关系是固定的,调用者将控制流转到被调用者,被调用者运行完毕后只能返回到调用者,而不能返回到其余协程。对称协程则否则。对称协程能够很容易由非对称协程来表达。且按通常的调用逻辑,A调B,B应返回到A,再由A发起到C的调用,而非B直接返回到C。所以,目前大多数协程库都只实现非对称协程。

  • 一种是借助glibc的ucontext,及相关的四个函数getcontext、setcontext、makecontext、swapcontext,如云风的库。固然这只能在linux环境下使用,在windows下,能够借助fiber实现相似的协程库;
  • 利用C标准库<setjmp.h>中的setjmp、longjmp实现协程。须要注意的是,setjmp仅负责保存寄存器的值,不负责维护其函数调用栈,这个须要另外实现;
  • 遵循规范从头实现。如libaco,它支持 Intel386 和 x86-64 两个平台的Sys V ABI,并提供了非对称协程的实现。关于Sys V ABI,It is today the standard ABI used by the major Unix operating systems such as Linux, the BSD systems, and many others. The Executable and Linkable Format (ELF) is part of the System V ABI. 也就是说,该协程库只支持类unix系统;
  • 使用汇编实现。较为著名的是Boost库,协程实现有两套:Corountine2和Corountine。Corountine2在Boost v1.59被引入,Boost.Corountine目前已被标记为deprecated。Boost.Corountine2使用了Boost.Context,所以要使用Boost.Corountine2,必须先编译Boost.Context。通用的C库tbox的协程模块也参照了Boost的实现。

关于汇编语法的平台差别,类Unix下采用的是AT&T的汇编语法格式,Dos/Windows下面采用的是Intel汇编语法格式。

 

参考资料:

云风-coroutine源码解析

System V ABI

Golang 协程调度

 

 

转载请注明本文出处:http://www.javashuo.com/article/p-hcsueoiu-gh.html

相关文章
相关标签/搜索