Search code examples
c++c++11boostboost-asioboost-coroutine

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

}

Solution

  • 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.