operation_canceled
is caused by canceling asynchronous operations.
Destructing the IO object, calling cancel
or emitting a cancellation signal on the bound cancellation_slot all potentially cancel asynchronous operations.
In addition, some IO objects document implicit cancellation. E.g. changing the expiration of a timer object implicitly cancels pending waits, e.g. expiress_from_now
:
Any pending asynchronous wait operations will be cancelled. The handler for each cancelled operation will be invoked with the boost::asio::error::operation_aborted error code.
Your Code: The Problem
Your code has a local variable timer
. The destructor runs before the function returns, cancelling the timer. Extend the lifetime to remove the problem!
auto timer = make_shared<asio::steady_timer>(ioc, 1s);
timer->async_wait([timer, &ioc](boost::system::error_code const& error) {
if (!error) {
std::cout << "Timer expired! Asynchronous wait complete." << std::endl;
waitForTimer(ioc); // Recursive call for infinite loop
} else {
std::cerr << "Error: " << error.message() << std::endl;
}
});
See it Live On Coliru
Although it seems more efficient/elegant to create a class that models the kind of recurring timer you want, prevent repeated instantiation and allocation.
BONUS: Encapsulate IntervalTimer
Makes everything easier to use and reusable. Here, demonstrating 2 simultaneous timers with different intervals (1.5s and 4s):
Live On Coliru
#include <boost/asio.hpp>
namespace asio = boost::asio;
using namespace std::chrono_literals;
struct IntervalTimer {
using timer = asio::steady_timer;
using duration = timer::duration;
using callback = std::function<void()>;
IntervalTimer(asio::any_io_executor ex, duration interval, callback cb)
: ival_(interval)
, timer_(ex, ival_)
, cb_(std::move(cb))
{
start();
}
private:
void start() {
timer_.expires_from_now(ival_);
timer_.async_wait([this](boost::system::error_code ec) {
if (!ec) {
if (cb_)
cb_();
start();
}
});
}
duration ival_;
timer timer_;
callback cb_;
};
#include <iostream>
#include <iomanip>
void trace(std::string_view msg) {
static constexpr auto now = std::chrono::steady_clock::now;
static auto const start = now();
std::cout << std::fixed << std::setprecision(2) << msg << " at " << (now() - start) / 1.s << "s"
<< std::endl;
}
int main() {
asio::io_context ioc;
// Start the initial timer wait
IntervalTimer timer1(ioc.get_executor(), 1500ms, []{ trace("timer 1 fired"); });
IntervalTimer timer2(ioc.get_executor(), 4s, []{ trace("timer 2 fired"); });
// Run the io_context to start asynchronous operations
ioc.run_for(10s);
}
Prints
timer 1 fired at 1.50s
timer 1 fired at 3.00s
timer 2 fired at 4.00s
timer 1 fired at 4.50s
timer 1 fired at 6.00s
timer 1 fired at 7.50s
timer 2 fired at 8.00s
timer 1 fired at 9.00s
BONUS 2: Dynamic Lifetime
To make IntervalTimer
have dynamic lifetime as well, you can employ enable_shared_from_this
: Live On Coliru
struct IntervalTimer : std::enable_shared_from_this<IntervalTimer> {
using timer = asio::steady_timer;
using duration = timer::duration;
using callback = std::function<void()>;
IntervalTimer(asio::any_io_executor ex, duration interval, callback cb)
: ival_(interval)
, timer_(ex, ival_)
, cb_(std::move(cb)) {}
void start() {
timer_.expires_from_now(ival_);
timer_.async_wait([this, self = shared_from_this()](boost::system::error_code ec) {
if (!ec) {
if (cb_)
cb_();
start();
}
});
}
private:
duration ival_;
timer timer_;
callback cb_;
};
With the same output.