尽管可以有不同的翻译,coroutine本文被叫作协程。
概念
Coroutine是相对于routine(过程)而提出的,它与一般的父程序调用子程序的routine不同之处在于,它允许挂起一个程序执行点,过后再从挂起的地方继续运行,是一种更高级的程序运行顺序控制。为保证程序挂起再恢复,协程有其自己的栈和控制块,记录挂起时的状态。
协程有如下特点:
- 同其他数据类型一样,协程也是第一类(first-class)对象,可以被当参数传递等操作;
- 运行特点是挂起运行,离开协程,过后再进入,恢复运行;
- 具有对称和非对称的转移控制机制
- 挂起前和恢复后本地变量的值是一致的;
- 有stackless和stackful两种类型
对称转移是指所有协程都是对等的,协程控制权跳转时,必须显式指明将控制权转给谁。非对称转移是指协程记录调用者,因此可以隐式转移控制权(给调用者)。
stackful协程能从嵌套的栈结构中恢复,而stackless协程,因为没有保存栈,因而只能在最上层(top-level)的过程中使用,被最上层过程调用的过程就不能再挂起了,也就是不能有两个协程同时被挂起。
为实现协程,一些语言特别引入了关键字,在语言层面支持协程,但目前C++不是如此,例如下文的Boost.Asio中,虽然定义了yield,但其实是一个宏。
协程与回调函数
采用协程与采用回调通常被用来对比。而它们通常与事件驱动式编程有关。
事件驱动式编程方式如下图:
有若干断续发生的事件源(source),若干个处理程序(handler),中间有一个分发/调度器(dispatcher),当有事件发生时,将其发给某个合适的处理程序进行处理。这里的事件一般是程序外部事件(如I/O),有特定的硬件进行侦测,因此不消耗CPU资源,也就是程序不用管,如果程序等在那,就是同步处理,如果做些别的事情,就是异步处理。
基于事件驱动的异步编程可以大大提高程序的处理效率,达到了“并行”的效果。基于事件驱动的异步编程可以采用多线程方式,也可以采用单线程+回调方式。后者的效率更高些,但会使代码执行碎片化—将一个连续过程切成若干hanlder,执行过程在handler函数间跳来跳去,同时,一些本应统一的处理,如错误、状态,就不得不深入每个handler,使得编程繁复,不直观。
Boost.Corountine2
Corountine2是相对于Corountine而言的,在Boost v1.59被引入,Boost.Corountine目前已被标记为deprecated,因此不再提及。
Boost.Corountine2使用了Boost.Context,因此要使用Boost.Corountine2,必须先编译Boost.Context。
Boost.Corountine2几个特征:
- 非对称转移控制(放弃了对称转移)
- stackful
- 对象只能移动(moveable),不能拷贝(copyable),因为协程对象控制的有些资源,如栈,只能独享
- coroutine<>::push_type 和coroutine<>::pull_type保存栈使用的是块式内存,可动态扩展,因此不用关心初始栈的大小,在析构时,所有的内存都被释放。
- 上下文切换通过coroutine<>::push_type::operator() ,coroutine<>::pull_type::operator()来完成,因此这两个函数内部不能再调用自身。
有两个创建协程的类:coroutine<>::pull_type和coroutine<>::push_type,pull的意思是从主运行环境可以“拉”数据到协程环境,push的意思是从协程环境将数据“推”到主运行环境中,coroutine<>::pull_type拉数据的方法是get(),coroutine<>::push_type推数据的方法是operator(),即在协程中,pull_type.get()可以得到外部传入的数据,而push_type()可以将数据传递到外部环境。为此,coroutine<>::pull_type提供了输入iterator及重载了std::begin()/std::end(),coroutine<>::push_type提供了输出iterator及重载了std::begin()/std::end(),方便它们的循环调用。
示例1如下。
int main()
{
typedef boost::coroutines2::coroutine<int> coro_t2;
std::cout<< "start corountine" << std::endl;
coro_t2::pull_type source( // constructor enters coroutine-function
[&](coro_t2::push_type& sink){
std::cout<< " sink1" << std::endl;
sink(1); // push {1} back to main-context
std::cout<< " sink2" << std::endl;
sink(2); // push {2} back to main-context
std::cout<< " sink3" << std::endl;
sink(3); // push {3} back to main-context
});
std::cout<< "start while" << std::endl;
while(source) // test if pull-coroutine is valid
{
int ret=source.get(); // pushed data,that is the argument of sink()
std::cout<< "move to coroutine-function "<< ret << std::endl;
source(); // context-switch to coroutine-function
std::cout<< "back from coroutine-function "<< std::endl;
}
return 0;
}
输出为:
start corountine
sink1
start while
move to coroutine-function 1
sink2
back from coroutine-function
move to coroutine-function 2
sink3
back from coroutine-function
move to coroutine-function 3
back from coroutine-function
该示例以coroutine<>::pull_type创建协程,以coroutine<>::push_type作为协程函数的参数,由于主环境中是pull,因此主环境中可以得到协程传出的数据,而主程序的数据只能通过全局变量来传递。程序跳转的变化通过输出可以清晰看出来:协程函数中的push函数(即sink)就是跳转点,下次再次进入协程函数时接着运行,主程序的跳转点就是pull函数(即source)。
示例2如下。
int main()
{
typedef boost::coroutines2::coroutine<int> coro_t3;
std::cout<< "start corountine" << std::endl;
coro_t3::push_type sink(
[&](coro_t3::pull_type& source){
int num;
num=source.get();
std::cout<< " source1: "<<num << std::endl;
source(); //back to main-context
std::cout<< " source2: "<<num << " "<<source.get()<< std::endl;
source();
std::cout<< " source3: "<<num << " "<<source.get()<< std::endl;
source();
});
std::cout<< "start while" << std::endl;
int i=1;
while(sink) // test if pull-coroutine is valid
{
std::cout<< "move to coroutine-function "<<i << std::endl;
sink(i); // context-switch to coroutine-function
std::cout<< "back from coroutine-function "<<i << std::endl;
i++;
}
return 0;
}
输出为:
start corountine
start while
move to coroutine-function 1
source1: 1
back from coroutine-function 1
move to coroutine-function 2
source2: 1 2
back from coroutine-function 2
move to coroutine-function 3
source3: 1 3
back from coroutine-function 3
move to coroutine-function 4
back from coroutine-function 4
该示例以coroutine<>::push_type创建协程,以coroutine<>::pull_type作为协程函数的参数,由于主环境中是push,因此主环境中可以将数据传递到协程,而协程数据只能通过全局变量来传递。注意到协程中的变量num的状态在运行中是得到保存的。程序的跳转通过输出也可以清晰看出,source/sink就是跳转点。该示例与示例1其实非常类似,说明coroutine<>::push_type和coroutine<>::pull_type其实是对等的,应用时注意两个区别:
- 数据传输方式不同,coroutine<>::pull_type用get(),而coroutine<>::push_type直接用参数。
- coroutine<>::pull_type在构造函数时,调用了一次协程函数(注意示例1的输出),而coroutine<>::push_type没有。
总结
协程的出现,改变了函数只有唯一运行入口的观点,一个较长的过程,尤其是过程中存在需要等待的步骤,也可以用一个函数来编写,等待的时候“跑”到函数外干点别的,然后再回来接着原来的步骤继续运行,如果采用回调的方式,需将这个过程切成一块一块的,块与块之间可以做点别的事情。
协程的出现使我们的编程方式多了一种选择,但协程的编程,尤其是与一些I/O异步操作结合起来用时,难度还是比较大的,因此我们通常并不直接使用coroutine库,Boost.Asio已经为我们封装了协程的功能,直接用Boost.Asio就好了。