如何在其链的上下文中恢复执行堆栈协程?

时间:2014-11-01 23:39:11

标签: c++ c++11 boost boost-asio

using Yield = asio::yield_context;
using boost::system::error_code;
int Func(Yield yield) {
  error_code ec;
  asio::detail::async_result_init<Yield, void(error_code, int)> init(yield[ec]);
  std::thread th(std::bind(Process, init.handler));
  int result = init.result.get();  // <--- yield at here
  return result;
}

如何实施Process以便Func将在Func最初产生的链的上下文中恢复?

4 个答案:

答案 0 :(得分:9)

Boost.Asio使用辅助函数asio_handler_invoke为调用策略提供自定义点。例如,当Handler被strand包装时,调用策略将导致在调用时通过strand调度处理程序。如documentation中所述,asio_handler_invoke应该通过参数依赖查找来调用。

using boost::asio::asio_handler_invoke;
asio_handler_invoke(nullary_functor, &handler);

对于堆栈协程,在生成协程时以及在调用与handler_type关联的yield_context以恢复协程时,需要考虑各种重要细节:

  • 如果代码当前正在协程中运行,那么它就在与协同程序相关联的strand范围内。本质上,一个简单的处理程序由strand包装,它恢复协程,导致执行跳转到协程,阻塞当前strand中的处理程序。当协同程序产生时,执行会跳回strand处理程序,允许它完成。
  • 虽然spawn()将工作添加到io_service(将启动并跳转到协程的处理程序),但协程本身不起作用。为了防止{/ 1}}事件循环在协程未完成时结束,可能需要在屈服之前将工作添加到io_service
  • 在调用 resume 之前,Stackful协程使用io_service来帮助保证协程产生。 Asio 1.10.6 / Boost 1.58能够从启动函数中安全地调用完成处理程序。先前版本要求未在启动函数内调用完成处理程序,因为其调用策略为dispatch(),导致协程在被挂起之前尝试恢复。

这是一个完整的example,说明了这些细节:

strand

输出:

#include <iostream>    // std::cout, std::endl
#include <chrono>      // std::chrono::seconds
#include <functional>  // std::bind
#include <thread>      // std::thread
#include <utility>     // std::forward
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>

template <typename CompletionToken, typename Signature>
using handler_type_t = typename boost::asio::handler_type<
  CompletionToken, Signature>::type;

template <typename Handler>
using async_result = boost::asio::async_result<Handler>;

/// @brief Helper type used to initialize the asnyc_result with the handler.
template <typename CompletionToken, typename Signature>
struct async_completion
{
  typedef handler_type_t<CompletionToken, Signature> handler_type;

  async_completion(CompletionToken&& token)
    : handler(std::forward<CompletionToken>(token)),
      result(handler)
  {}

  handler_type handler;
  async_result<handler_type> result;
};

template <typename Signature, typename CompletionToken>
typename async_result<
  handler_type_t<CompletionToken, Signature>
>::type
async_func(CompletionToken&& token, boost::asio::io_service& io_service)
{
  // The coroutine itself is not work, so guarantee the io_service has
  // work.
  boost::asio::io_service::work work(io_service);

  // Initialize the async completion handler and result.
  async_completion<CompletionToken, Signature> completion(
      std::forward<CompletionToken>(token));

  auto handler = completion.handler;
  std::cout << "Spawning thread" << std::endl;
  std::thread([](decltype(handler) handler)
    {
      // The handler will be dispatched to the coroutine's strand.
      // As this thread is not running within the strand, the handler
      // will actually be posted, guaranteeing that yield will occur
      // before the resume.
      std::cout << "Resume coroutine" << std::endl;
      using boost::asio::asio_handler_invoke;
      asio_handler_invoke(std::bind(handler, 42), &handler);
    }, handler).detach();

  // Demonstrate that the handler is serialized through the strand by
  // allowing the thread to run before suspending this coroutine.
  std::this_thread::sleep_for(std::chrono::seconds(2));

  // Yield the coroutine.  When this yields, execution transfers back to
  // a handler that is currently in the strand.  The handler will complete
  // allowing other handlers that have been posted to the strand to run.
  std::cout << "Suspend coroutine" << std::endl;
  return completion.result.get();
}

int main()
{
  boost::asio::io_service io_service;

  boost::asio::spawn(io_service,
    [&io_service](boost::asio::yield_context yield)
    {
      auto result = async_func<void(int)>(yield, io_service);
      std::cout << "Got: " << result << std::endl;
    });

  std::cout << "Running" << std::endl;
  io_service.run();
  std::cout << "Finish" << std::endl;
}

有关详情,请考虑阅读Library Foundations for Asynchronous Operations。它提供了有关异步操作组合的更多详细信息,Running Spawning thread Resume coroutine Suspend coroutine Got: 42 Finish 如何影响Signature以及async_resultasync_resulthandler_type的整体设计。< / p>

答案 1 :(得分:4)

以下是基于Tanner的优秀答案的Boost 1.66.0的更新示例:

#include <iostream>    // std::cout, std::endl
#include <chrono>      // std::chrono::seconds
#include <functional>  // std::bind
#include <thread>      // std::thread
#include <utility>     // std::forward
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>

template <typename Signature, typename CompletionToken>
auto async_add_one(CompletionToken token, int value) {
    // Initialize the async completion handler and result
    // Careful to make sure token is a copy, as completion's handler takes a reference
    using completion_type = boost::asio::async_completion<CompletionToken, Signature>;
    completion_type completion{ token };

    std::cout << "Spawning thread" << std::endl;
    std::thread([handler = completion.completion_handler, value]() {
        // The handler will be dispatched to the coroutine's strand.
        // As this thread is not running within the strand, the handler
        // will actually be posted, guaranteeing that yield will occur
        // before the resume.
        std::cout << "Resume coroutine" << std::endl;

        // separate using statement is important
        // as asio_handler_invoke is overloaded based on handler's type
        using boost::asio::asio_handler_invoke;
        asio_handler_invoke(std::bind(handler, value + 1), &handler);
    }).detach();

    // Demonstrate that the handler is serialized through the strand by
    // allowing the thread to run before suspending this coroutine.
    std::this_thread::sleep_for(std::chrono::seconds(2));

    // Yield the coroutine.  When this yields, execution transfers back to
    // a handler that is currently in the strand.  The handler will complete
    // allowing other handlers that have been posted to the strand to run.
    std::cout << "Suspend coroutine" << std::endl;
    return completion.result.get();
}

int main() {
    boost::asio::io_context io_context;

    boost::asio::spawn(
        io_context,
        [&io_context](boost::asio::yield_context yield) {
            // Here is your coroutine

            // The coroutine itself is not work, so guarantee the io_context
            // has work while the coroutine is running
            const auto work = boost::asio::make_work_guard(io_context);

            // add one to zero
            const auto result = async_add_one<void(int)>(yield, 0);
            std::cout << "Got: " << result << std::endl; // Got: 1

            // add one to one forty one
            const auto result2 = async_add_one<void(int)>(yield, 41);
            std::cout << "Got: " << result2 << std::endl; // Got: 42
        }
    );

    std::cout << "Running" << std::endl;
    io_context.run();
    std::cout << "Finish" << std::endl;
}

输出:

Running
Spawning thread
Resume coroutine
Suspend coroutine
Got: 1
Spawning thread
Resume coroutine
Suspend coroutine
Got: 42
Finish

说明:

  • 大力利用Tanner的回答
  • 首选网络TS命名(例如,io_context)
  • boost :: asio提供了一个async_completion类,它封装了handler和async_result。小心,因为处理程序接受对CompletionToken的引用,这就是现在显式复制令牌的原因。这是因为通过async_result(completion.result.get)产生的结果将使相关的CompletionToken放弃其基础强引用。这最终会导致意外提前终止协程。
  • 明确指出单独的using boost::asio::asio_handler_invoke声明非常重要。显式调用可以防止调用正确的重载。

-

我还要提到我们的应用程序最终得到了两个协同程序可以与之交互的io_context。特别是一个用于I / O绑定工作的上下文,另一个用于CPU。使用带有boost::asio::spawn的显式链最终使我们能够很好地控制协程将运行/恢复的上下文。这有助于我们避免零星的BOOST_ASSERT(!is_running())失败。

使用显式链创建一个协同程序:

auto strand = std::make_shared<strand_type>(io_context.get_executor());
boost::asio::spawn(
    *strand,
    [&io_context, strand](yield_context_type yield) {
        // coroutine
    }
);

调用显式调度到strand(multi io_context world):

boost::asio::dispatch(*strand, [handler = completion.completion_handler, value] {
    using boost::asio::asio_handler_invoke;
    asio_handler_invoke(std::bind(handler, value), &handler);
});

-

我们还发现,在async_result签名中使用future将允许在恢复时将异常传播回协程。

using bound_function = void(std::future<RETURN_TYPE>);
using completion_type = boost::asio::async_completion<yield_context_type, bound_function>;

产量为:

auto future = completion.result.get();
return future.get(); // may rethrow exception in your coroutine's context

答案 2 :(得分:0)

通过Boost Asio提供的执行程序框架创建线程,使事情变得复杂。

出于这个原因,你不应该假设你想要的是可能的。我强烈建议您只需向io_service添加更多主题,然后让它为您管理主题。

或者,您可以扩展库并添加您显然想要的新功能。如果是这样,最好联系开发人员邮件列表获取建议。也许他们欢迎这个功能¹?


¹(有趣的是,你没有描述,所以我不会问它的目的是什么)

答案 3 :(得分:0)

using CallbackHandler = boost::asio::handler_type<Yield, void (error_code, int)>::type;

void Process(CallbackHandler handler) {
  int the_result = 81;
  boost::asio::detail::asio_handler_invoke(
      std::bind(handler, error_code(), the_result), &handler);
}

@sehe暗示,我制定了上述工作解决方案。但我不确定这是否是正确/惯用/最佳方式。欢迎发表评论/编辑此答案。