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.
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
.cancel[_one]() on your IO object if it has that member function).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);
#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);
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
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.
#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 :)