摘要: 本文介绍协程的基本概念,以及协程在异步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++——它提供了更强大的封装能力。

协程和异步
标签: