How to wait for either of two timers to finish (Boost Asio)

The code below prints to the console when both timer1 and timer2 have finished. How can I change it to print when either timer1 or timer2 finishes, and then cancel the other timer.

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

int main() {

  boost::asio::io_context io;

  boost::asio::deadline_timer timer1(io, boost::posix_time::seconds(5));
  boost::asio::deadline_timer timer2(io, boost::posix_time::seconds(1));

  boost::asio::spawn(io, [&](boost::asio::yield_context yield){
    timer1.async_wait(yield);
    timer2.async_wait(yield);
    std::cout << "Both timer1 and timer2 have finished" << std::endl;
  });

  io.run();

}

Answer

I took the question to mean “how do you async_wat_any(timer1, timer2, ..., yield).

The other answer is correct in pointing at callback completion-handlers to provide this, but they don’t provide the glue back to a single coroutine.

Now Asio’s async operations abstract away the difference between all the invocation styles (callback, use_future, use_awaitable, yield_context etc…) – bringing them all back under the “callback” style essentially.

Therefore you can make your own async intiation that ties these torgether, rough sketch:

template <typename Token>
auto async_wait_any( std::vector<std::reference_wrapper<timer>> timers, Token token) {
    using Result =
        boost::asio::async_result<std::decay_t<Token>, void(error_code)>;
    using Handler  = typename Result::completion_handler_type;

    Handler handler(token);
    Result result(handler);

    for (timer& t : timers) {
        t.async_wait([=](error_code ec) mutable {
            if (ec == boost::asio::error::operation_aborted)
                return;
            for (timer& t : timers) {
                t.cancel_one();
            }
            handler(ec);
        });
    }

    return result.get();
}

Now in your coroutine you can say:

timer a(ex, 100ms);
timer b(ex, 200ms);
timer c(ex, 300ms);

async_wait_any({a, b, c}, yield);

and it will return when the first one completes.

Let’s Demo

Also, making it more generic, not hard-coding the timer type. In fact on a Windows environment you will be able to wait on Waitable Objects (like Event, Mutex, Semaphore) with the same interface:

template <typename Token, typename... Waitable>
auto async_wait_any(Token&& token, Waitable&... waitable) {
    using Result =
        boost::asio::async_result<std::decay_t<Token>, void(error_code)>;
    using Handler = typename Result::completion_handler_type;

    Handler completion_handler(std::forward<Token>(token));
    Result result(completion_handler);

    // TODO use executors from any waitable?
    auto ex = get_associated_executor(
        completion_handler,
        std::get<0>(std::tie(waitable...)).get_executor());

    auto handler = [&, ex, ch = completion_handler](error_code ec) mutable {
        if (ec != boost::asio::error::operation_aborted) {
            (waitable.cancel_one(), ...);
            post(ex, [=]() mutable { ch(ec); });
        }
    };

    (waitable.async_wait(bind_executor(ex, handler)), ...);

    return result.get();
}

We’ll write a demo coroutine like:

int main() {
    static auto logger = [](auto name) {
        return [name, start = now()](auto const&... args) {
            ((std::cout << name << "t+" << (now() - start) / 1ms << "mst") << ... << args) << std::endl;
        };
    };

    boost::asio::io_context ctx;
    auto wg = make_work_guard(ctx);

    spawn(ctx, [log = logger("coro1"),&wg](yield_context yield) {
        log("started");

        auto ex = get_associated_executor(yield);
        timer a(ex, 100ms);
        timer b(ex, 200ms);
        timer c(ex, 300ms);

        log("async_wait_any(a,b,c)");
        async_wait_any(yield, a, b, c);

        log("first completed");
        async_wait_any(yield, c, b);
        log("second completed");
        assert(a.expiry() < now());
        assert(b.expiry() < now());

        // waiting again shows it expired as well
        async_wait_any(yield, b);

        // but c hasn't
        assert(c.expiry() >= now());

        // unless we wait for it
        async_wait_any(yield, c);
        log("third completed");

        log("exiting");
        wg.reset();
    });

    ctx.run();
}

This prints Live On Coliru

coro1   +0ms    started
coro1   +0ms    async_wait_any(a,b,c)
coro1   +100ms  first completed
coro1   +200ms  second completed
coro1   +300ms  third completed
coro1   +300ms  exiting

Notes, Caveats

Tricky bits:

  • It’s hard to decide what executor to bind the handlers to, since there could be multiple associated executors. However, since you’re using coroutines, you’ll always get the correct strand_executor associated with the yield_context

  • It’s important to do the cancellations before invoking the caller’s completion token, because otherwise the coroutine is already resumed before it was safe, leading to potential lifetime issues

  • Speaking of which, since now we post async operations outside the coroutine with the coroutine suspended, we will need a work-guard, because coroutines are not work.