I tried to restart a boost asio timer but it is not working as expected
The code is based on www.boost.org/doc/libs/1_84_0/doc/html/boost_asio/reference/steady_timer.html#boost_asio.reference.steady_timer.changing_an_active_waitable_timer_s_expiry_time
The output is
0 : Start at
0 : ioContext.run()
1000 : on_timeout Timeout reached error=0
1100 : Too late, timer has already expired after 1100 ms
2200 : We managed to cancel the timer after 1100 ms
3300 : We managed to cancel the timer after 1100 ms
...
but should be something like:
0 : Start at
0 : ioContext.run()
1000 : on_timeout Timeout reached error=0
1000 : Too late, timer has already expired after 1100 ms
2000 : on_timeout Timeout reached error=0
2000 : Too late, timer has already expired after 1100 ms
3000 : on_timeout Timeout reached error=0
3000 : Too late, timer has already expired after 1100 ms
...
#include <chrono>
#include <functional>
#include <iostream>
#include <thread>
#include <boost/asio.hpp>
long int millis() {
static long int offset = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() - offset;
}
class BoostAsioTimerEval {
public:
BoostAsioTimerEval(boost::asio::io_context& ioContext)
: ioContext_(ioContext), timer_(ioContext_), interval_(std::chrono::milliseconds{1000}), lastMillis_(millis()) {
timer_.expires_after(interval_);
timer_.async_wait(std::bind(&BoostAsioTimerEval::on_timeout, this, std::placeholders::_1));
std::cout << millis() << " : Start at " << std::endl;
}
void on_event() {
if (timer_.expires_after(interval_) > 0) {
std::cout << millis() << " : We managed to cancel the timer after " << (millis() - lastMillis_) << " ms" << std::endl;
timer_.async_wait(std::bind(&BoostAsioTimerEval::on_timeout, this, std::placeholders::_1));
} else {
std::cout << millis() << " : Too late, timer has already expired after " << (millis() - lastMillis_) << " ms" << std::endl;
// kick again
timer_.expires_after(interval_);
timer_.async_wait(std::bind(&BoostAsioTimerEval::on_timeout, this, std::placeholders::_1));
}
lastMillis_ = millis();
}
void on_timeout(const boost::system::error_code& error) {
if (error != boost::asio::error::operation_aborted) {
std::cout << millis() << " : on_timeout Timeout reached error=" << error.value() << std::endl;
} else {
std::cout << millis() << " : on_timeout Timeout aborted error=" << error.value() << std::endl;
}
}
private:
boost::asio::io_context& ioContext_;
boost::asio::steady_timer timer_;
std::chrono::milliseconds interval_;
long int lastMillis_;
};
int main(int argc, char** argv) {
boost::asio::io_context ioContext;
BoostAsioTimerEval boostAsioTimerEval(ioContext);
std::thread worker([&]() {
for (int i = 0; i < 10; ++i) {
const int sleep = 1100;
std::cout << millis() << " : sleep for " << sleep << " ms" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(sleep));
boostAsioTimerEval.on_event();
}
ioContext.stop();
});
std::cout << millis() << " : ioContext.run()" << std::endl;
ioContext.run();
worker.join();
return 0;
}
Firstly I sanitized the code. So
millis()could actually just readreturn (now() - offset) / 1ms.¹Also taking care to avoid TOCTOU race conditions with the timer updates, so:
I arrived at Live On Coliru
Indeed it prints:
I must admit I've never used the return value of
expires_after. Instead I'd like to observe that NONE of theon_timeoutinvocations actually showasio::error::operation_aborted("Operation canceled") as the ec. So something else is wrong!Regardless, your line commented
// kick againreally makes no sense, because it sets the expiry again, after just having done that, realizing that nothing was canceled.Let's simplify
on_eventto:Undefined Behaviour
The first really issue I see is that you have UB (you're accessing
timerfrom multiple threads without synchronization. Now, when you fix that, you will notice something interesting:The Real Problem
Running that simply prints
The reason is, after the first timer completion (
on_timeout), the service runs out of work, because your thread isn't work. Indeed, if you add a little trace:You get
That's your problem.
Work Guards
There are several ways to fix this, but let's choose the most "low-level" one first:
The
workguard keeps the service alive at least until the end of the worker. In fact, you can drop theioc.stop()as it is redundant!Live On Coliru
Prints
In fact, reducing the delay (to e.g. 900ms) now shows that the
nreturned fromexpires_atis actually accurate. It was just the fact that the service wasn't running.¹ I don't know why people keep hurting themselves with those heinously ridiculous incantations like
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count()- just what now ¯\(ツ)/¯