使用带有Boost C++ 类库的C++ 协同程序

原文发表地址:using C++ Coroutines with Boost C++ Libraries

原文发表时间:2017/5/19

上个月,Jim Springfield 写了一篇关于在Libuv中使用C++ 协同程序的文章(一个用于异步I/O的多平台C类库)。本月我们将介绍如何使用Boost C++ 类库的组件协同工作,即boost::future和boost::asio。

获得Boost

如果你已经安装了boost,你可以跳过这一步。如果还没有安装,建议你使用vcpkg快速地在你的电脑上安装boost。你可以按照如下指令获取vcpkg 然后输入如下命令行安装32位或者64位的boost版本:

.\vcpkg install boost boost:x64-windows

为了确保安装正确,请打开VS并且创建一个C++ Win控制台程序:

当你运行程序时,它应该打印出42。

Boost::Future: 协同程序部分

当编译器在处理一个函数中的co_await, co_yield 或者co_return时,它将这个函数视为协同程序。就C++本身来说,它并没有C++协同程序语义的含义,用户或者类库编写者需要提供一个std::experimental::coroutine_traits模板特例化去告诉编译器应该做什么。(编译器通过传递返回值的类型和所有传递给函数的参数的类型)。

我们希望能够编写协同程序以返回boost::future。为了做到这些,我们将按照以下方式特例化coroutine_traits:

当协同程序暂停时,需要返回一个可以满足当协程程序运行到结束或者结束时返回异常的future。

成员函数promise_type::get_return_object解释了怎么获取一个能够连接到特定实例的协同程序的future。成员函数promise_type::set_exception说明了如果在协同程序中发生了未处理的异常时会发生什么。在我们的示例中,我们希望把异常存储在与我们从协同程序中返回的future连接着的promise中。

成员函数promise_type::return_void 说明了当执行到co_return语句或者控制流运行到协同程序末尾的时候会发生什么。

成员函数initial_suspend和final_suspend,正如我们定义的那样,告诉编译器,在它被调用后并且我们要立即开始执行协同程序并且一旦运行完就销毁协同程序。

为了控制非空futures,定义boost::future的特例化任意类型:

注意在这种情况下我们定义了return_value,和前边的例子中的return_void不同。它告诉编译器我们期望一个协同程序最终返回一些非空值(通过一个co_return语句)并且这些值将会被传递到与该协同程序相关联的future。(这两个专门化之间有很多常见的代码;如果需要的话它可以被分离出来)。

现在我们准备好要测试了。在命令行选项增加编译选项“/await”以便启用协同程序在编译器中的支持(因为协同程序还不是C++标准的一部分,所以需要明确的选择性加入去启用它们)。

另外,添加一个支持协同程序的include文件, 这些文件主要定义了std::experimental::coroutine中对我们比较重要的实例化模板:

当程序运行时,它应该打印:“Hi”和42。

Boost::Future: Await部分

下一步是向编译器解释如果你想在boost::future尝试‘await’该怎么做。

给出一个需要await的表达式,编译器需要知道三件事:

  1. 准备好了吗?
  2. 如果准备好了,怎么获得结果?
  3. 如果没有准备好,怎么预定能够当它准备好时得到通知?

为了得到这些问题的答案,编译器会寻找三个成员函数:await_ready能够返回‘true’或者‘false’,当表达式准备好获得结果时编译器将会调用await_resume(调用await_resume的结果会成为整个await表达式的结果),并且最终,编译器会调用await_suspend()函数以便当结果准备就绪时得到通知。并且会传递一个用于恢复或者销毁协同程序的协同程序句柄。

在boost::future的情况下,它有给出答案的功能,但是它没有像上一段描述的那样的必须的成员函数。为了解决这个问题,我们可以定义一个可以把boost::future有的转化为编译器想要的东西的运算符co_await。

在这种情况下,当future准备就绪时,协同程序会通过await_suspend绕过暂停并且立即通过await_resume获得结果。

根据应用,有一种最为有效的方法。比如你正在编写一个客户端应用程序,当future已经准备好了的时候你的程序自然会运行的比较快一点,你免去了暂停之后由boost::future 协同函数所产生的时间消耗。。在服务器应用程序中,随着你的服务器处理数成百个同步请求,当它接收请求时,如果协同程序依据公平准则被启动, 那么处理请求需要的响应时间是可以被预测的, 这将会意义重大。 我们可以假想在一个进度条里面, 部分协同程序运行良好, 一旦它们开始请求是否ready 状态时, 程序的future 结束。 然而这样的协同程序将会独占线程资源而导致其他用户资源紧张。

你可以挑选任何一种你喜欢的方式并且尝试我们的品牌新运算符co_await:

像往常一样,当你运行这个片段时,它将打印出42。注意在函数f里我们不再需要co_return。由于await表达式的存在编译器会知道这是一个协同程序。

Boost::asio

使用我们迄今为止开发的适配器,你现在可以自由地使用返回boost::future的协同程序并且处理任何APIs和返回boost::future的类库。但是如果你有一些不返回boost::future并且使用回调作为延续机制的类库呢?

作为模型,我们将使用boost::asio::system_timer的成员函数async_wait。没有协同程序,你可以按照如下所示使用system_timer:

当你运行这个程序时,它会打印出“waiting for a tick”,100毫秒之后会紧跟着输出“tick”。

让我们围绕timer’s async_await创建一个封装能够使它在伴随协同程序时也可用。我们希望能够使用这个结构使指定的定时器暂停执行所需的持续时间:

整体结构看起来和我们如何为boost::future定义运算符co_await一样。我们需要从async_awiat返回一个对象来告诉编译器什么什么暂停,什么时候唤醒什么是运算符的结果。

注意当我们构造Awaiter时我们传递参数t和参数d。我们需要把这两个参数存储在Awaiter里以便我们可以在await_ready和await_suspend成员函数里访问它们。

另外你可能注意到在system_timer示例中async_await有一个用来接收表示是否完成等待或者出现错误(例如定时器被取消)的错误码的参数。我们需要向awaiter添加一个成员变量去存储错误码直到它被await_resume取消。

成员函数await_ready将会告诉我们是否需要暂停。如果我们完成如下设置,如果等待时间为0时我们将会告诉编译器不要暂停一个协同程序。

在await_suspend中,我们将调用timer.async_await去预定一个延续。当boost::asio提示我们时我们将会记住错误代码并且返回协同程序。

最后,当一个协同程序被回复时,我们将会检查错误代码并且如果wait不成功时我们会将其当作异常。

并且为了方便起见,整个适配器一体化:

一个简单的使用它的示例:

当你运行它的时候,它会在100毫秒分别打印出tick1,tick2和tick3。

总结

我们快速浏览了如何开发能够使用现有C++库的协同程序的适配器。请尝试,并且试着添加更多的适配器。另外即将发布的博客文章:了解如何使用boost::asio的CompletionToken traits去创建一个协同程序适配器而没有手动写入。