Search code examples
c++c++11boostboost-asiocoroutine

Wait for timer in other coroutine (Asio)


When using asio::spawn, it possible to wait for a timer in a separate coroutine? E.g., in the code below I want coroutine 2 started to print to the console and then, 5 seconds later, coroutine 2 finished.

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

int main() {

  boost::asio::io_context io;

  // coroutine 1
  boost::asio::spawn(io, [&](boost::asio::yield_context yield) {
    boost::asio::deadline_timer timer(io, boost::posix_time::seconds(5));
    timer.async_wait(yield);
  });

  // coroutine 2
  boost::asio::spawn(io, [&](boost::asio::yield_context yield) {
    std::cout << "coroutine 2 started" << std::endl;
    // wait for coroutine 1 timer to finish
    std::cout << "coroutine 2 finished" << std::endl;
  });

  io.run();
}

Solution

  • Many ways, most would center on sharing the timer.

    Easiest way I can think of is by spawning coro2 from coro1 ("forking", if you will), because why not:

    Live On Wandbox

    #include <boost/asio.hpp>
    #include <boost/asio/spawn.hpp>
    #include <iostream>
    
    using namespace std::chrono_literals;
    using boost::asio::yield_context;
    using timer = boost::asio::steady_timer;
    
    static auto now = timer::clock_type::now;
    
    static void coro_sleep(timer::duration delay, yield_context yc) {
        timer(get_associated_executor(yc), delay)
            .async_wait(yc);
    }
    
    int main() {
        static constexpr auto never = timer::time_point::max();
    
        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;
        spawn(ctx, [log = logger("coro1")](yield_context yield) {
            log("started");
            timer cond(get_associated_executor(yield), 5s);
    
            spawn(yield, [&cond, log = logger("coro2")](yield_context yield) {
                log("started");
    
                cond.async_wait(yield);
                log("condition met");
    
                coro_sleep(1200ms, yield);
                log("signal done");
                cond.expires_at(never);
    
                log("exiting");
            });
    
            for (; cond.expiry() != never; coro_sleep(1s, yield)) {
                log("alive");
            }
    
            log("exiting");
        });
    
        ctx.run();
    }
    

    Prints

    coro1   +0ms    started
    coro2   +0ms    started
    coro1   +0ms    alive
    coro1   +1001ms alive
    coro1   +2001ms alive
    coro1   +3001ms alive
    coro1   +4001ms alive
    coro2   +5000ms condition met
    coro1   +5002ms alive
    coro1   +6002ms alive
    coro2   +6200ms signal done
    coro2   +6200ms exiting
    coro1   +7002ms exiting
    

    Of course there are many ways to share the timer if you so require, but the principle will be the same. Pay attention to synchronization of shared resources when you are using multiple threads to run the execution context(s).

    The way I phrased it, a simple change would make sure of this:

    Live On Wandbox

    boost::asio::thread_pool ctx;
    spawn(make_strand(ctx), [log = logger("coro1")](yield_context yield) {