Boost中的协程—Boost.Coroutine2

尽管可以有不同的翻译,coroutine本文被叫作协程。

概念

Coroutine是相对于routine(过程)而提出的,它与一般的父程序调用子程序的routine不同之处在于,它允许挂起一个程序执行点,过后再从挂起的地方继续运行,是一种更高级的程序运行顺序控制。为保证程序挂起再恢复,协程有其自己的栈和控制块,记录挂起时的状态。

协程有如下特点:

  1. 同其他数据类型一样,协程也是第一类(first-class)对象,可以被当参数传递等操作;
  2. 运行特点是挂起运行,离开协程,过后再进入,恢复运行;
  3. 具有对称和非对称的转移控制机制
  4. 挂起前和恢复后本地变量的值是一致的;
  5. 有stackless和stackful两种类型

对称转移是指所有协程都是对等的,协程控制权跳转时,必须显式指明将控制权转给谁。非对称转移是指协程记录调用者,因此可以隐式转移控制权(给调用者)。

stackful协程能从嵌套的栈结构中恢复,而stackless协程,因为没有保存栈,因而只能在最上层(top-level)的过程中使用,被最上层过程调用的过程就不能再挂起了,也就是不能有两个协程同时被挂起。

为实现协程,一些语言特别引入了关键字,在语言层面支持协程,但目前C++不是如此,例如下文的Boost.Asio中,虽然定义了yield,但其实是一个宏。

 

协程与回调函数

采用协程与采用回调通常被用来对比。而它们通常与事件驱动式编程有关。

事件驱动式编程方式如下图:

Boost中的协程—Boost.Coroutine2_第1张图片

些,但会使代码执行碎片化—将一个连续过程切成若干hanlder,执行过程在handler函数间跳来跳去,同时,一些本应统一的处理,如错误、状态,就不得不深入每个handler,使得编程繁复,不直观。

 

Boost.Corountine2

Corountine2是相对于Corountine而言的,在Boost v1.59被引入,Boost.Corountine目前已被标记为deprecated,因此不再提及。

Boost.Corountine2使用了Boost.Context,因此要使用Boost.Corountine2,必须先编译Boost.Context。

Boost.Corountine2几个特征:

  1. 非对称转移控制(放弃了对称转移)
  2. stackful
  3. 对象只能移动(moveable),不能拷贝(copyable),因为协程对象控制的有些资源,如栈,只能独享
  4. coroutine<>::push_type 和coroutine<>::pull_type保存栈使用的是块式内存,可动态扩展,因此不用关心初始栈的大小,在析构时,所有的内存都被释放。
  5. 上下文切换通过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   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   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: "<

该示例以coroutine<>::push_type创建协程,以coroutine<>::pull_type作为协程函数的参数,由于主环境中是push,因此主环境中可以将数据传递到协程,而协程数据只能通过全局变量来传递。注意到协程中的变量num的状态在运行中是得到保存的。程序的跳转通过输出也可以清晰看出,source/sink就是跳转点。该示例与示例1其实非常类似,说明coroutine<>::push_type和coroutine<>::pull_type其实是对等的,应用时注意两个区别:

  1. 数据传输方式不同,coroutine<>::pull_type用get(),而coroutine<>::push_type直接用参数。
  2. coroutine<>::pull_type在构造函数时,调用了一次协程函数(注意示例1的输出),而coroutine<>::pull_type没有。

总结

协程的出现,改变了函数只有唯一运行入口的观点,一个较长的过程,尤其是过程中存在需要等待的步骤,也可以用一个函数来编写,等待的时候“跑”到函数外干点别的,然后再回来接着原来的步骤继续运行,如果采用回调的方式,需将这个过程切成一块一块的,块与块之间可以做点别的事情。

协程的出现使我们的编程方式多了一种选择,但协程的编程,尤其是与一些I/O异步操作结合起来用时,难度还是比较大的,因此我们通常并不直接使用coroutine库,Boost.Asio已经为我们封装了协程的功能,直接用Boost.Asio就好了。

 

 

你可能感兴趣的:(C,Boost,Coroutine,C/C++)