如何让自己的异步库应用 yield_context


#1

asio 1.54 开始,支持了 boost.coroutine 作为底层机制衍生的 boost::asio::spawn() 接口,可以创建 stackful 协程。


void  this_coroutine(boost::asio::yield_context yielder)
{
   boost::asio::streambuf buf;
boost::asio::ip::tcp::socket mysocket;

  boost::asio::ip::tcp::endpoint endpoint("someip",  someport);

  boost::asio::async_connect(mysocket, endpoint, yielder);

   size_t  bytes_transfered =  boost::asio::async_read_until(mysocket, buf, "\r\n", yielder);
 
    boost::asio::async_write(mysocket, boost::asio::buffer(buf, bytes_transfered), yielder);
}

int main()
{
  boost::asio::io_service  io;
 boost::asio::spawn(io, this_coroutine);
 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);

 boost::asio::spawn(io, this_coroutine);


  io.run();
  return 0;
}

上面创建了 10 个协程去给对方压力测试, 呵呵,

使用 spawn 传进来的 asio::yield_context 作为 async_* 系的 回调,即可自动的完成协程切换,回调被调用后,协程又被切换回来。 非常easy。

那么,我们自己写的库,比如说 avhttp , 要如何实现支持 yield_context 呢? 难道是模版特化,为 yield_context 单独再写个 async_** :question:

当然不是拉 :exclamation:

要支持 yield_context ,首先看看, 传统上, async_* 是怎么定义和实现的:

template<typename Handler>
void async_nimei(nimei_t nimei, Handler handler)
{
    nimei_op<Handler>(nimei, handler)();
}

执行 async_nimei 就是创建一个 nimei_op<Handler> 对象, 传入不同的 Handler 类型,对 template classs nimei_op; 这个类型实例化。 nimei_op 通过成员 Handler m_handler; 保存传进来的回调。 nimei_op 完成操作后, 即调用 m_handler(ec, … ) 就把回调给调用了。

问题在于, yield_context 可不是函数对象,继续使用这种写法,肯定是不行的。而且 异步操作如果是协程模式,居然是有返回值的!

通过阅读 asio 的源码,发现了解决办法。

首先, 函数的声明得这样

template<class Handler>
	inline BOOST_ASIO_INITFN_RESULT_TYPE(Handler,void(boost::system::error_code,  nimei_result_t  ))

async_nimei(nimei_t nimei, Handler handler)

通过宏 BOOST_ASIO_INITFN_RESULT_TYPE(Handler,void(boost::system::error_code,  nimei_result_t  )) 表面两点:

第一点, handler 的类型必须是 void handler(boost::system::error_code,  nimei_result_t);

第二点来啦! 如果 handdler 是 yield_context :heavy_exclamation_mark: :heavy_exclamation_mark: :heavy_exclamation_mark: 表示 async_nimei 的返回值不再是 void :heavy_exclamation_mark: :heavy_exclamation_mark: :heavy_exclamation_mark: 而是 nimei_result_t :heavy_exclamation_mark: :heavy_exclamation_mark: :heavy_exclamation_mark:

这个是通过复杂的模版特化实现的,目前暂时不仔细探讨实现原理。

好了,函数的声明可以了,那么内部怎么做呢 :question:

使用 boost::asio::detail::async_result_init :exclamation: :heavy_exclamation_mark:

using namespace boost::asio;
boost::asio::detail::async_result_init<Handler, void(boost::system::error_code, nimei_result_t)> init(BOOST_ASIO_MOVE_CAST(Handler)(handler));

构造了一个 init 对象,这个对象封装了外部的 handler , 这个 init 对象就是这里的 magic 了,如果外部的对象是个 bind 返回的,或者是 lambda 啊,又或者手鲁的函数对象啊,init 只是一个 wraper, 本身没干啥事情。

但是如果 外部的 handler 实际上是个 yield_context 就不一样了,在这里, init 变成了一个 协程的调度器。

然后马上在下一行代码里用到

nimei_io<BOOST_ASIO_HANDLER_TYPE(Handler, void(boost::system::error_code,nimei_result_t))>(nimei, init.handler)();

在这里,使用 BOOST_ASIO_HANDLER_TYPE 宏来执行 type decude 获取 nimei_op 模版类的Handler 的实例化类型。然后使用 init.handler 作为 handler 调用 nimei_op 。

接下来一行就是 magic 发生的地方了。

return init.result.get();

就是这个 init.result.get(); 完成了将回调的第二个参数返回的功能!! 但是如果 handler 不是 yield_context , 那么 init.result.get() 又其实只是个空操作。。。。。。而且返回值也是 void !

好了,完整代码如下

template<class Handler>
inline BOOST_ASIO_INITFN_RESULT_TYPE(
   Handler,void(boost::system::error_code,  nimei_result_t)
) async_nimei(nimei_t nimei, Handler handler)
{
 using namespace boost::asio;
 detail::async_result_init<Handler, void(boost::system::error_code, nimei_result_t)> init(BOOST_ASIO_MOVE_CAST(Handler)(handler));

 nimei_io<
 BOOST_ASIO_HANDLER_TYPE(Handler, void(boost::system::error_code,nimei_result_t))
 >(nimei, init.handler)();
 return init.result.get();
}

虽然多写了几行代码,但是支持了 yield_context 和普通的 handler ,实在是妙极了。


#2

boost 1.66 带的 ASIO 变化较大, 这个模式需要修正。具体的来说就是要把 detail::async_result_init 换成 boost::asio::async_completion