2

The below simple async_wait example gives Operation canceled error.

#include <iostream>
#include <boost/asio.hpp>

void waitForTimer(boost::asio::io_context& ioContext) {
    boost::asio::steady_timer timer(ioContext, boost::asio::chrono::seconds(1));

    timer.async_wait([&](const boost::system::error_code& error) {
        if (!error) {
            std::cout << "Timer expired! Asynchronous wait complete." << std::endl;
            waitForTimer(ioContext);  // Recursive call for infinite loop
        } else {
            std::cerr << "Error: " << error.message() << std::endl;
        }
    });
}

int main() {

    std::cout << "Using Boost "     
            << BOOST_VERSION / 100000     << "."  // major version
            << BOOST_VERSION / 100 % 1000 << "."  // minor version
            << BOOST_VERSION % 100                // patch level
            << std::endl;


    boost::asio::io_context ioContext;

    // Start the initial timer wait
    waitForTimer(ioContext);

    // Run the io_context to start asynchronous operations
    ioContext.run();    

    return 0;
}
Using Boost 1.82.0
Error: Operation canceled

I just want to run that simple async_wait example with infinite loop. But it doesnt work.

1 Answers1

2

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.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    Added two bonus examples showing an encapsulated `IntervalTimer` and how to allow completely dynamic object lifetime safely. – sehe Aug 02 '23 at 14:04