4

I have the following program that just runs a timer for 5 seconds and has a signal handler.

#include <boost/asio.hpp>
#include <chrono>
#include <iostream>
#include <signal.h>

boost::asio::io_context io;

void signal_handler(
    const boost::system::error_code& ec,
    int signal_number)
{
    std::cout << "Received signal " << signal_number << " [" << ec << "]" << std::endl;
}

boost::asio::awaitable<void> session()
{
    try {
        boost::asio::steady_timer t(io, boost::asio::chrono::seconds(5));
        boost::system::error_code ec;
        t.wait(ec); // 1
        // co_await t.async_wait(boost::asio::redirect_error(boost::asio::use_awaitable, ec)); // 2
        std::cout << "Socket connect timed out [" << ec << "]" << std::endl;
    }
    catch (...) {
        std::cout << "Exception" << std::endl;
    }

    co_return;
}

int main(int argc, char *argv[]) {
    boost::asio::signal_set signals(io, SIGINT);

    signals.async_wait(signal_handler);
    boost::asio::co_spawn(io, session(), boost::asio::detached);

    io.run();
}

When I generate SIGINT the program exits immediately with the following log:

^CSocket connect timed out [system:4]
Received signal 2 [system:0]

The initial version uses synchronous timer and uses it by calling wait methond. When I comment line // 1 and uncomment line // 2 and generate SIGINT programs exits after 5 seconds with the following log:

^CReceived signal 2 [system:0]
Socket connect timed out [system:0]

Second version uses async_wait method and coroutine approach to trigger the 5 seconds wait.

I would like to understand why there is a difference in behavior, what is happening under the hood and which one is more natural to the asio library.

1 Answer 1

5

First off, using a synchronous (i.e. blocking) timer inside a coroutine is invalid use of coroutines: the service will not be able to schedule any operations because it is purposely blocked by the programmer.

Second, your signal handler never attempts to stop the coroutine. So it continuous on.

If you to cancel one operation when another operation completes, you should

  • manually wire them up to a cancellation signal (or indeed invoke .cancel[_one]() on your IO object if it has that member function).
  • use make_parallel_group to achieve the same. As a syntactic sugar you can use awaitable_operators to combine operations

Using Awaitable Operators

Clearly this is the simplest to do:

asio::signal_set signals(io, SIGINT);
co_spawn(io, session() || signals.async_wait(asio::use_awaitable), asio::detached);

Live On Coliru

#include <boost/asio.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>
namespace asio = boost::asio;
using namespace std::chrono_literals;
using namespace asio::experimental::awaitable_operators;

asio::awaitable<void> session() try {
    asio::steady_timer        t(co_await asio::this_coro::executor, 5s);
    boost::system::error_code ec;
    co_await t.async_wait(asio::redirect_error(ec)); // 2
    std::cout << "Completed [" << ec << "]" << std::endl;
} catch (...) {
    std::cout << "Exception" << std::endl;
}

int main() {
    asio::io_context io;

    asio::signal_set signals(io, SIGINT);
    co_spawn(io, session() || signals.async_wait(asio::use_awaitable), asio::detached);

    io.run();
}

When run with time (./a.out& sleep 1.5; kill -INT %1; wait) prints

Completed [system:125]
real    0m1.504s
user    0m0.000s
sys 0m0.004s

To also handle other signals etc. wrap a second coro:

asio::awaitable<void> signal_waiter() {
auto [ec, signal_number] =                                                         //
co_await asio::signal_set(co_await asio::this_coro::executor, SIGINT, SIGTERM) //
            .async_wait(asio::as_tuple);
std::cout << "Received signal " << signal_number << " [" << ec << "]" << std::endl;
}

so you can simply (Live)

co_spawn(io, session() || signal_waiter(), asio::detached);

Using Parallel Group

Same deal, but more typing:

asio::experimental::make_parallel_group( //
    co_spawn(io, session), co_spawn(io, signal_waiter))
    .async_wait(asio::experimental::wait_for_one{}, asio::detached);

See it Live On Coliru

Using Cancellation Slots

This demonstrates binding a cancellation slot to the coro executor as well as demonstrating invoking .cancel() on the signal_set to signal when the session completed on its own.

Live On Coliru

#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
using namespace std::chrono_literals;

asio::awaitable<void> session() try {
    asio::steady_timer        t(co_await asio::this_coro::executor, 5s);
    boost::system::error_code ec;
    co_await t.async_wait(asio::redirect_error(ec)); // 2
    std::cout << "Completed [" << ec << "]" << std::endl;
} catch (...) {
    std::cout << "Exception" << std::endl;
    co_return;
}

int main() {
    asio::io_context io;

    asio::cancellation_signal on_term;

    asio::signal_set signals(io, SIGINT, SIGTERM);
    signals.async_wait([&](boost::system::error_code ec, int signum) {
        std::cout << "Received signal " << ::strsignal(signum) << " [" << ec << "]" << std::endl;
        if (!ec)
            on_term.emit(asio::cancellation_type::all);
    });

    co_spawn(io, session,
             bind_cancellation_slot(on_term.slot(), //
                                    [&](std::exception_ptr) { signals.cancel(); }));

    io.run();
}

Sidenote: Don't use global variables :)

Sign up to request clarification or add additional context in comments.

4 Comments

Added the full set of examples for the different approaches (they're ordered from high-level to low-level in this answer)
thanks for your response. Can you elaborate why the programs stops immediately in the first case and waits for timer in the second.
It's not clear to me what you meant by "first case" or "second case". Regardless, the usage of timer.wait() is just demonstrably wrong, so I'd disregard any "observations" made from that scenario.
Okay, having looked at it: first scenario happens to report the system_error (interrupted syscall) on Ctrl-C, not due to any code of your own. The second scenario I already answered at the top my answer: "your signal handler never attempts to stop the coroutine.". So, nothing happens, you just ignored the signal (with a console message).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.