MaxPlankton
MaxPlankton

Reputation: 1248

Multiple recursive async_wait on boost asio steady_timer

This question is inspired by the tutorial on asynchronous timer from boost asio documentation (link). The code is slightly modified to make the effect more obvious.

There is a related question, Multiple async_wait from a boost Asio deadline_timer. But I am not sure if the answer in that question applies to my case.

The code is pretty simple and works as expected if the duplicated line is commented out, as shown below.

  1. A steady_timer with a duration of 1s calls async_wait.

  2. When it expires, the handler is called. Inside the handler, the lifetime of the timer is extended by one more second, and the timer calls async_wait again.

  3. Variable count of 20 is used to limit number of times the timer can fire off.


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

namespace asio = boost::asio;

void bind_handler(const boost::system::error_code& ec,
                  asio::steady_timer& t,
                  int count) {
  if (count > 0) {
    std::cout << "getting " << count << "\n";
    t.expires_at(t.expiry() + std::chrono::seconds(1));
    t.async_wait(boost::bind(bind_handler, asio::placeholders::error,
                             boost::ref(t), --count));
  }
}

int main() {
  asio::io_context io_context(1);
  asio::steady_timer t(io_context, std::chrono::seconds(1));

  int count = 20;

  t.async_wait(boost::bind(bind_handler, asio::placeholders::error,
                           boost::ref(t), count));
  //t.async_wait(boost::bind(bind_handler, asio::placeholders::error,
  //                         boost::ref(t), count));

  auto start = std::chrono::steady_clock::now();
  io_context.run();
  auto end = std::chrono::steady_clock::now();
  std::cout
      << std::chrono::duration_cast<std::chrono::seconds>(end - start).count()
      << " seconds passed\n";

  return 0;
}

The output of this code is shown below. A new line is printed with each second that passes.

getting 20
getting 19
getting 18
...lines...
...omitted...
getting 3
getting 2
getting 1
21 seconds passed

However, if two lines in the code above are uncommented, the program behaves very differently. The output is pasted below. The program prints all lines from getting 20 to getting 1 within a second, shows nothing for 40 seconds and then prints the last line.

getting 20
getting 20
getting 19
getting 19
getting 18
getting 18
...lines...
...omitted...
getting 3
getting 3
getting 2
getting 2
getting 1
getting 1
41 seconds passed

My question is, how does multiple recursive call of async_wait affect the behavior of the program? I feel some kind of data race is going on, but the numbers are still printed sequentially. Besides, only a single thread is involved, as we can see in io_context constructor.

Upvotes: 2

Views: 3192

Answers (1)

Matthias247
Matthias247

Reputation: 10396

It seems like the answer for the behavior lies in the documentation for basic_waitable_timer::expires_at(const time_point & expiry_time):

This function sets the expiry time. Any pending asynchronous wait operations will be cancelled. The handler for each cancelled operation will be invoked with the boost::asio::error::operation_aborted error code.

In your example, when the first timer finishes, it calls expires_at in order to forward the timer a second. However this cancels the second running await call, which now will be called directly in the next eventloop iteration with an operation_aborted error. However since you don't check the error code ec, you don't see that. Now this handler will directly forward the timer again, and thereby cancel the last async_wait that had been started.

This goes on, until the handlers cancelled themselves often enough that count==0 and only a single timer is running. Since the expiry date has been forwarded each time by 1s, the code still waits for the full 40s to elapse.

Upvotes: 6

Related Questions