Asio 协程详解


#1

所谓协程

协程, 即保持现场的可重入函数. 第二次调用的时候会在上次返回的地方继续, 而不是从头开始执行. 为了体现协程的这个特点, 许多内置协程支持的语言一般使用 yield 代替 return 作返回.

asio 协程, stackless 版

C++ 语言并不支持协程, 但是 asio 通过 Duff’s Device 技术, 再配合必包来封持上下文, 做到了 stackless 协程.

具体来说, 实现的手段是这样的:


struct this_is_a_coroutine
{

   void operator()()
   {
       switch(state)
       {
            case 0:
               do some thing;
               //call some function
               async_xxx(*this); // note *this as callback
               state = 1;
               break;
           case 1:
               // asybc_xxx 回调后代码从这里继续!
               do other thing
               async_xxx(*this);
               state = 2;
               break;
               for ( i = 0; i < s ; i++)
               {
                   async_xxx(*this);
                   state = 3;
                   break;
                   case 3:               
               }
             // do other things.
            break;
       }
   }

   int state;

   int i; int s;
   and more local variables;
}


在上面这个代码里, 使用了 switch case 语句来进行状态恢复. 在 for 循环内部套了一个 case 标签, 而这个case 居然是能真的直接跳进去的. 这个反常的跳转就是 Duff’s Device , 他可以让我们实现在 for 和 while 循环内调用 async_* 系并让协程状态回来.

但是手写 switch 并不直观, 并不能直观的让人感受到, 这是个协程. ASIO 作者于是将 定义 SWITCH 和 CASE 的地方使用 reenter 和 yield 两个宏表示. 上面的代码用 asio 的宏改写为如下表示



struct this_is_a_coroutine :  boost::asio::coroutine
{

   void operator()()
   {
       reenter(this)
       {
           do some thing;
           //call some function
           yield async_xxx(*this); // note *this as callback
           // asybc_xxx 回调后代码从这里继续!
           do other thing
           yield async_xxx(*this);
           for ( i = 0; i < s ; i++)
           {
               yield async_xxx(*this);
           }
           // do other things.
       }
   }

   int i; int s;
   and more local variables;
}


代码瞬间就看起来简洁多了.

由于 reenter 和 yield 可能会和其他代码定义的符号冲突的情况,
所以 asio 把 reenter 宏定义放到了 boost/asio/yield.hpp 里,然后可以使用 boost/asio/unyield.hpp 取消定义.

如果不想引起符号冲突, 可以不保护 yield.hpp 只使用 BOOST_ASIO_CORO_REENTER 和 BOOST_ASIO_CORO_YIELD 两个大写的宏.

asio 协程, stackful 版

刚刚介绍的协程, 不需要任何编译器/底层库的支持. 只使用 C 语言本身就许可的 Duff’s Device 技术就能实现. 唯一的缺点是局部变量无法跨 yield . 所以所有变量都要定义为函数对象的成员变量. 另外需要把协程定义为函数对象, 需要额外编写不少代码, 另一个简单的协程做法是使用底层汇编代码实现 CPU 上下文的切换. 正如系统内核切换CPU状态模拟出来的线程一样. 所以这种办法又叫用户态线程. Windows 上管它叫纤程(Fiber)

Boost 提供了一个 Boost.Context 库, 就是用的汇编代码实现的 CPU 上下文切换. asio 利用这个库封装出了一个简单易用的协程, stackful 协程.

还是上面那个协程, 改为 stackful 版本后代码如下


void this_is_a_coroutine(boost::asio::yield_context ctx)
{
    do some thing;
    //call some function
    async_xxx(ctx);
    // asybc_xxx 回调后代码从这里继续!
    do other thing
    async_xxx(ctx);
    for ( i = 0; i < s ; i++)
    {
        async_xxx(ctx);
    }
}

这样就简单多了. 对于 asio 的回调来说, 如果是 handler( boost::system::error_code , other ) 这样的回调形式, 如果使用 yield_context 那么, other 就是 返回值, 直接使用即可. error_code 会被转换为异常. 例如

async_read 的回调是 handler(boost::system::error_code, size_t bytes_transfreed) 那么使用 yield_context 做回调, 则 size_t bytes_transfreed = async_read(***, ctx) 这样用就可以了.

参考资料:

https://www.avboost.com/t/yield-context/516


#2