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