ma-hnln
ma-hnln

Reputation: 173

ASIO C++ coroutine cancellation

I am spawning a coroutine as shown below.

asio::co_spawn(executor, my_coro(), asio::detached);

How am I supposed to cancel it?

As far as I know, per handler cancellation can be achieved simply by binding a handler with asio::bind_cancellation_slot. This does not work in my specific example (irrespective of using the asio::detached "handler" or some lambda), and it makes sense to me that it does not work.

I found this blog post which basically says that spawning the coroutine from another one and awaiting it (with appropriate asio::bind_cancellation_slot setup) does the trick. This fails for me as my_coro never seems to run in the example below. Besides, I don't even know why this should work at all (with respect to cancellation).

asio::awaitable<void> cancelable(asio::cancellation_signal& sig, asio::awaitable<void>&& awaitable) {
    co_await asio::co_spawn(co_await asio::this_coro::executor, std::move(awaitable), asio::bind_cancellation_slot(sig.slot(), asio::use_awaitable));
    co_return;
}

// ...

asio::cancellation_signal signal;
asio::co_spawn(executor, cancelable(signal, my_coro()), asio::detached);

What is the correct way to connect a asio::cancellation_signal to the asio::cancellation_state of a coroutine (obtained by co_await asio::this_coro::cancellation_state) in ASIO?

If it helps: I'm using ASIO stand-alone on the most recent master, i.e. not the boost version.

Upvotes: 5

Views: 3149

Answers (1)

Koronis Neilos
Koronis Neilos

Reputation: 782

To cancel a coroutine in asio use the new awaitable_operator '||'. The awaitable operator '||' allows to co_await for more than one coroutine until one of the coroutines is finished. For example:

#include "boost/asio/experimental/awaitable_operators.hpp"
#include <boost/asio.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/system_timer.hpp>
#include <boost/asio/this_coro.hpp>
#include <chrono>
#include <iostream>

boost::asio::awaitable<void>
printHelloEverySecond ()
{
  for (;;)
    {
      std::cout << "Hello" << std::endl;
      boost::asio::system_timer timer{ co_await boost::asio::this_coro::executor };
      timer.expires_after (std::chrono::seconds{ 1 });
      co_await timer.async_wait (boost::asio::use_awaitable);
    }
}

boost::asio::awaitable<void>
stopAfter4500Milliseconds (boost::asio::system_timer &timer)
{
  boost::asio::system_timer stopAfterFiveSeconds{ co_await boost::asio::this_coro::executor };
  stopAfterFiveSeconds.expires_after (std::chrono::milliseconds{ 4500 });
  co_await stopAfterFiveSeconds.async_wait (boost::asio::use_awaitable);
  timer.cancel ();
}

boost::asio::awaitable<void>
cancel (boost::asio::system_timer &timer)
{
  try
    {
      co_await timer.async_wait (boost::asio::use_awaitable);
    }
  catch (boost::system::system_error &e)
    {
      using namespace boost::system::errc;
      if (operation_canceled == e.code ())
        {
          //  swallow cancel
        }
      else
        {
          std::cout << "error in timer boost::system::errc: " << e.code () << std::endl;
          abort ();
        }
    }
}

int
main ()
{
  boost::asio::io_context ioContext{};
  boost::asio::system_timer timer{ ioContext };
  timer.expires_at (std::chrono::system_clock::time_point::max ());
  using namespace boost::asio::experimental::awaitable_operators;
  co_spawn (ioContext, printHelloEverySecond () || cancel (timer), [] (auto, auto) { std::cout << "stopped" << std::endl; });
  co_spawn (ioContext, stopAfter4500Milliseconds (timer), boost::asio::detached);
  ioContext.run ();
}

Here "printHelloEverySecond ()" will run until "cancel()" returns successfully which happens when the timer is canceled (in "stopAfter4500Milliseconds") after 4500 milliseconds. This allows to cancel "printHelloEverySecond ()" when the timer gets canceled. For reference:
Chris Kohlhoff explains awaitable_operator and cancelation

Upvotes: 6

Related Questions