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

902 Views Asked by At

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();

}
2

There are 2 best solutions below

3
sehe On BEST 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 << "ms\t") << ... << 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.

7
ravenspoint On

How about:

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

void print_timer_expired( bool& flag)
{
 if( flag )
     return;
 flag = true;
 std::cout << "Timer1 or timer2 has finished" << std::endl;
}
int main() {

  boost::asio::io_context io;
  bool flag = false;  // true if message has been printed

  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);
    print_timer_expired( flag );
  });
  boost::asio::spawn(io, [&](boost::asio::yield_context yield){
    timer2.async_wait(yield);
    print_timer_expired( flag );
  });

  io.run();

}