使用C++协同程序与Boost C++库

这篇文章是戈尔尼沙诺夫写的。

null

上个月,吉姆·斯普林菲尔德写了一篇很棒的文章 关于C++与Libuv协同程序的探讨 (用于异步I/O的多平台C库)。本月我们将研究如何使用BooS+C++库的组件,即Booo::

得到提升

如果你已经有了 促进 如果已安装,请跳过此步骤。否则,我建议使用 vcpkg公司 快速将boost安装到您的机器上。跟随 说明 获取vcpkg,然后输入以下行以安装32位和64位版本的boost:

.vcpkg install boost boost:x64-windows

要确保所有安装正确,请打开 并创建一个C++ Win32控制台应用程序:

[code lang=“cpp”]#定义BOOSTu THREADu PROVIDESu FUTURE#define BOOSTu THREADu PROVIDESu FUTUREu CONTINUATION//启用FUTURE::then#包括#包括#包括

使用名称空间boost;使用名称空间boost::asio;

int main(){io服务io;承诺p;自动f=p.get_future();

io.post([&]{p.set_值(42);});io.run();

printf(“%%d“,f.get());}

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

未来:协同程序部分

当编译器遇到 等待 , 一氧化碳产量 co U返回 在函数中,它将函数视为一个协程。C++本身并不定义协同程序的语义,用户或库编写器需要提供一个专门化的 标准::实验::协同程序特征 告诉编译器要做什么的模板(编译器通过传递返回值的类型和传递给函数的所有参数的类型来实例化协同程序(coroutine)。

我们希望能够作者协同程序,返回一个未来。为了做到这一点,我们将专门研究coroutineu特性,如下所示:

[code lang=“cpp”]模板struct std::experimental::coroutineu traits,Args…>{结构类型{boost::承诺p;auto get_return_object(){return p.get_future();}std::experimental::suspendu never initialu suspend(){return{};}std::experimental::suspendu never finalu suspend(){return{};}void set_exception(std::exception_ptr e){p.set_exception(std::move(e));}void return_void(){p.set_value();}};};

当一个协同程序被挂起时,它需要返回一个将来,当这个协同程序运行到完成或者异常完成时,它将得到满足。

成员函数 promise_type::get_return_object 定义如何获得将连接到协同程序的特定实例的未来。 成员函数 promise_type::set_exception 定义在协同程序中发生未处理的异常时发生的情况。在我们的例子中,我们希望将这个异常存储到与我们从协同程序返回的未来相关的承诺中。

成员函数 promise_type::return_void 定义当执行到达 co U返回 语句或控制流运行到协同程序的末尾。

成员函数 initial_suspend final_suspend ,当我们定义它们时,告诉编译器我们希望在调用协同程序后立即开始执行它,并在它运行到完成时销毁它。

要处理非无效未来,请为任意类型的boost::future定义专门化:

[code lang=“cpp”]模板struct std::experimental::coroutineu traits,Args…>{结构类型{boost::promisep;auto get_return_object(){return p.get_future();}std::experimental::suspendu never initialu suspend(){return{};}std::experimental::suspendu never finalu suspend(){return{};}void set_exception(std::exception_ptr e){p.set_exception(std::move(e));}模板void返回值(U&&U){p、 设置_值(std::forward(U));}};};

注意,在本例中,我们定义了 return_value ,与 return_void 就像前面的例子一样。这告诉编译器,我们期望协同程序最终需要返回一些非空值(通过 co_return 语句),并且该值将传播到与此协同例程相关联的未来(这两个专门化之间有很多共同的代码;如果需要的话,它可以被分解出来)。

现在,我们准备测试一下。添加一个“/等待”命令行选项,以便在编译器中支持协同程序(因为协同程序不是C++标准的一部分,需要显式选择)来打开它们。

另外,为定义的主模板的coroutine支持头添加include std::experimental::coroutine_traits 我们想专攻:

[code lang=“cpp”]#包括

[code lang=“cpp”]//…包括和专业化的协同程序特性…

boost::futuref(){“嗨!”;co U返回;}

boost::未来g(){返回42;}

int main(){f().get();printf(“%%d“,g().get());};

当它运行时,它应该打印:“嗨!”42岁。

未来:等待部分

下一步是向编译器解释如果您试图在boost::future上“等待”该怎么做。

给定一个待处理的表达式,编译器需要知道三件事:

  1. 准备好了吗?
  2. 如果准备好了,如何得到结果。
  3. 如果它还没有准备好,那么当它准备好时如何订阅以获得通知。

要获得这些问题的答案,编译器将查找三个成员函数: await_ready() 应该返回“true”或“false”, await_resume() 当表达式准备好获取结果(调用 await_resume() 成为整个await表达式的结果),最后是awaitu suspend(),编译器将调用该函数以在结果准备就绪时获取通知,并将传递可用于恢复或销毁协同路由的协同路由句柄。

在boost::future的情况下,它有提供答案的工具,但是它没有上一段中描述的必需的成员函数。为了解决这个问题,我们可以定义 operator co_await 这可以将boost::future所拥有的内容转换为编译器想要的内容。

[code lang=“cpp”]模板自动操作员cou await(boost::future&&f){结构等待器{boost::未来&&input;boost::未来输出;bool await_ready(){return false;}auto await_resume(){return output.get();}void awaitu suspend(std::experimental::coroutineu handle<>coro){输入。然后([this,coro](auto resultu future){this->output=std::move(结果未来);coro.resume();});}};return waiter{staticu cast&&>(f)};}

注意,在上面的适配器中,我们总是返回 false await_ready() ,即使它*已*就绪,也会强制编译器始终调用awaitu suspend to subscribe,以便通过future::then获得延续。另一种方法是编写waitu ready,如下所示:

[code lang=“cpp”]bool waiting u ready(){if(input.isu ready()){输出=标准::移动(输入);返回true;}返回false;}

在这种情况下,如果未来已经准备好,协程将通过 await_suspend 并立即通过 await_resume .

根据应用情况,一种方法可能比另一种更有益。例如,如果您正在编写一个客户机应用程序,那么您的应用程序自然会运行得快一点,如果在未来已经准备好的时候,您不必先暂停,然后再由boost::future恢复协同例程。在服务器应用程序中,服务器处理数百个同时发生的请求,总是通过。如果总是以公平的方式安排连续性,则可能会产生更可预测的响应时间,这可能是有益的。很容易想象这样一种连胜:某个特定的协同程序总是幸运的,当它询问是否准备好时,它的未来就已经完成了。这样的协同程序将占用线程,并可能使其他客户机陷入饥饿。

选择您喜欢的任何方法,并尝试我们全新的运营商合作伙伴:

[code lang=“cpp”]//…包括,协同程序特性的专门化,操作员协同等待。

boost::未来g(){返回42;}

boost::futuref(){printf(“%%d“,co_wait g());}

int main(){f().get();};

像往常一样,当你运行这个片段时,它将打印42。注意,我们不再需要 co_return 在功能上 f . 编译器知道它是一个协程,因为存在一个等待表达式。

助推::asio

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

作为模型,我们将使用boost::asio::systemu计时器的asyncu wait成员函数。如果没有协程,您可以按如下方式使用系统计时器:

[code lang=“cpp”]#包括#包括

使用名称空间boost::asio;使用名称空间std::chrono;

int main(){io服务io;系统定时器(io);

计时器。从现在起过期(100ms);timer.asyncu wait([](boost::system::erroru code ec){if(ec)printf(“计时器失败:%%d“,ec.value());否则(“勾选”);});

puts(“等待滴答声”);io.run();};

当你运行这个程序时,它会打印“等待滴答”,然后在100毫秒后打印“滴答”。让我们围绕计时器的asyncu wait创建一个包装器,使其可用于协同路由。我们希望能够使用这种结构:

[code lang=“cpp”]同步等待异步等待(计时器,100ms);

使用指定的计时器在所需的时间内暂停其执行。整体结构将类似于我们如何定义操作符coïu waiting for boost::future。我们需要从asyncu wait返回一个对象,该对象可以告诉编译器何时挂起、何时唤醒以及操作的结果。

[code lang=“cpp”]模板自动异步等待(boost::asio::systemu timer&t,std::chrono::durationd){结构等待者{};返回等待者{t,d};}

注意,我们在构造waiter时传递参数t和d。我们需要将它们存储在awaiter中,以便在awaitu ready和awaitu suspend成员函数中访问它们。

[code lang=“cpp”]boost::asio::系统定时器&t;标准::时间::持续时间d;

另外,您可能注意到在systemu timer示例中,asyncu wait的完成回调有一个参数,该参数接收一个错误代码,指示等待是成功完成还是有错误(例如,计时器被取消)。我们需要向waiter添加一个成员变量来存储错误代码,直到它被 await_resume .

[code lang=“cpp”]boost::system::错误代码ec;

成员函数awaitu ready将告诉我们是否需要暂停。如果我们实现它如下,我们将告诉编译器,如果等待时间为零,就不要挂起协同程序。

[code lang=“cpp”]bool await u ready(){返回d.count()==0;}

在awaitu suspend中,我们将调用timer.asyncu await来订阅一个延续。当boost::asio给我们回电话时,我们将记住错误代码并恢复协同进程。

[code lang=“cpp”]void awaitu suspend(std::experimental::coroutineu handle<>coro){t、 从现在起过期(d);t、 异步等待([this,coro](auto ec){此->ec=ec;coro.resume();});}

最后,当恢复协同程序时,我们将检查错误代码,如果等待不成功,则将其作为异常进行传播。

[code lang=“cpp”]作废等待恢复(){如果(ec)抛出boost::system::systemu error(ec);}

为了您的方便,整个适配器为一体:

[code lang=“cpp”]模板自动异步等待(boost::asio::systemu timer&t,std::chrono::durationd){结构等待器{boost::asio::系统定时器&t;标准::时间::持续时间d;boost::system::错误代码ec;

bool await u ready(){返回d.count()==0;}作废等待恢复(){如果(ec)抛出boost::system::systemu error(ec);}void awaitu suspend(std::experimental::coroutineu handle<>coro){t、 从现在起过期(d);t、 异步等待([this,coro](auto ec){此->ec=ec;coro.resume();});}};返回等待者{t,d};}

举个小例子:

[code lang=“cpp”]//…包括,协同特性的专门化等。

使用名称空间boost::asio;使用名称空间std::chrono;

boost::futuresleepy(io服务和io){系统定时器(io);同步等待异步等待(计时器,100ms);卖出(“1”);同步等待异步等待(计时器,100ms);卖出(“2”);同步等待异步等待(计时器,100ms);卖出(“3”);}

int main(){io服务io;困倦(io);io.run();};

当您运行它时,它应该以100毫秒的间隔打印tick1、tick2和tick3。

结论

我们快速地研究了如何开发适配器,使其能够使用现有的C++库的协同程序。请试用,并尝试添加更多适配器。另外,请关注即将发布的关于如何使用boost::asio的CompletionToken特性来创建协同路由适配器的博客文章,而不必手动编写它们。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享