摘要: 本文介绍协程的基本概念,以及协程在异步IO编程模式里起的作用——大大简化异步回调的实现与逻辑处理。
什么协程
协程这个概念在计算机科学里算是一个老概念了,随着现代计算机语言与多核心处理器的普及,似乎也有普及之势。协程是与例程相对而言的。
熟悉C/C++语言的人都知道,一个例程也就是一个函数。当我们调用一个函数时,执行流程进入函数;当函数执行完成后,执行流程返回给上层函数或例程。期间,每个函数执行共享一个线程栈;函数返回后栈顶的内容自动回收。这就是例程的特点,也是现代操作系统都支持这种例程方式。
协程与例程相对,从抽象的角度来说,例程只能进入一次并返回一次,而协程可能进入多次并返回多次。比如说,我们有下面一段程序:
void fun ( int val )
{
int a = 0 ; //1
int b = 0 ; //2
int c =a +b ; //3
}
如果上面的代码是一个例程,那么它只能把 1、2、3 依次执行后,才返回。如果是协程,它可能在 1 处暂停,然后在某个时刻从 2 处继续执行;接着在 2 处执行完之后暂停,然后在另外一个时刻从 3 处继续执行。
从抽象角度,协程就这么简单。
异步IO的特点与分析
在了解协程的特点(可以多次进入同一个函数,并接着上次运行处继续执行)后,我们再来考虑一下,这一特点如何应用到异步IO程序中。在异步IO程序中,有很大一块代码是处理异步回调的,也就是数据读取或写入由系统执行,当任务完成后,系统会执行用户的回调。如果只是很少使用这种回调,那么程序并不会因为异步而复杂多少,但要是程序中异步回调大量存在,那么此时我们会发现,原本简单的程序可能因为回调而变得支离破碎,原本一个简单的循环,现在需要写入多个函数,并在多个函数里来回调用。下面示例一下:
//下面代码片断是同步代码,它从IO读一段数据,并把这段数据写回
void start ( )
{
for ( ;; )
{
Buffer buf ;
read (buf ) ; //把书读到buf
write (buf ) ; //把buf的数据写回
}
//注意到没有,同步代码很简单直接,一个循环,几行代码完成全部事务
}
//把上面的同步代码映射为异步,代码量可能要增加很多,并且程序逻辑也变得不清晰
//示例如下
//读回调,在回调里我们发起写操作
void readHandle (buf )
{
writeAsync (buf, writeHandle ) ;
}
//写回调,在回调里我们发起读操作
void writeHandle (buf )
{
readAsync (buf, readHandle ) ;
}
//开始循环
void start ( )
{
static Buffer buf ; //buf变量不能在栈上,为了简单这里写成静态变量
readAsync (buf, readHandle ) ;
}
从上面的代码比较中,我们可以看出异步IO会把代码分隔成许多碎片,同时原本清晰的处理逻辑也因为被放入多个函数里,而变得很不清晰。上面的同步代码,一个了解程序的初级程序员也可以读懂写出,但相同功能的异步代码,一个初级程序员可能就搞不定了,甚至很难搞明白为什么要这么做。
读到这里,对异步不是太了解的人可能会问,既然异步把问题搞复杂了,那我们为什么还要用异步呢?答案简单有力,为了“性能”。只有这一个原因,当程序需要处理大量IO时,异步的效率将高出同步代码许多倍。如何一个程序的性能不其关心部分,那真不应该使用异步IO。
对比我们的异步IO代码与其功能相同的同步代码,我们发现每个异步调用都是要把代码分隔一个小函数——比原本要小的函数,当异步调用返回后,我们又接着下面处理。这一点跟协程很像,在一个协程里,当发起异步IO时,我让它返回,当异步IO完成后,我让这个协程接着执行,处理余下的逻辑。
协程与异步结合——性能与简单的结合
结合上面的分析,如果我们可以写下面功能的代码,将很完美:
void start ( )
{
for ( ;; )
{
Buffer buf ;
yeild readAsync (buf,start ) ;
//------ 分隔线,协程在这里返回,等待readAsync完成,当readAsync完成后,它再调用start
//此时start将从这里接着运行
yeild writeAsync (buf, start ) ;
//------ 分隔线,协程在这里返回,等待writeAsync完成,当writeAsync完成后,它再调用start
//此时start将从这里接着运行
}
}
上面的代码也很一清晰明了。如果真的能写这样的代码,那将是所有程序员之福。这样在一些语言里确实可以直接写出来,比如Python、Lua、golang,这些语言有原生的协程支持——编译器或解释器为我们处理了这些执行流的跳转。那么在C/C++里怎么实现呢?可能肯定的是,C/C++里不能直接写出这样简洁的代码,但却可以写类似的代码,尤其是C++——它提供了更强大的封装能力。
C/C++里怎么实现协程
根据我的理解,C/C++里不能直接实现这样的能力,尤其是上面的那种可以保持栈上内容的实现。协程在进入细分的时候,人们把分为两类: stackless coroutine 和 stackfull coroutine。前者在C/C++里使用宏可以模拟实现,后者在C/C++里调用汇编可以实现。boost库是C++世界里非常著名的库,它就实现了这两个方式的协程。在最新的boost 1.53 里,它提供了一个stackfull协程,而boost.asio的作者在它的示例里定义一组宏,完成了stackless协程的实现。后者可能是风格跟boost库不一致,所以没有变成boost库的一部分,而只作为一个示例出现。
从思路上来说,stackless 协程就是自动记录程序当前运行到的位置,在下次运行时候使用goto之前语句完成跳转,但因为这实现上是一个函数全新的实例,函数栈也是全新的,所以跟之前运行的实例没有关系。这也是stackless由来。stackfull 协程则运用汇编把CPU当前的几个相关的寄存器值恢复或保存来完成在函数里定位,因为这要求每一个协程都必须一个独立的内在作为其运行
外部链接: