Boost ASIO asynchronous wait on timer

Having seen how ASIO takes care of resources, now we are ready for something a bit more spicy, setting up an asynchronous wait.

Plain example

Firstly, I have faithfully followed the Timer.2 Boost ASIO tutorial, only using standard C++ construct instead of the Boost counterpart.

We want ASIO to run in another thread the following simple function, that just does some output, and we want it to be done after a certain delay.
namespace bs = boost::system;

// ...

void hello(const bs::error_code& ec)  // 1
 std::cout << "delayed hello [" << ec.value() << "] " << std::flush;
1. If ASIO completes the wait on the timer correctly, it passes an error code with value zero.

namespace ba = boost::asio;
namespace sc = std::chrono;

// ...

void timer2(ba::io_context& io)  // 1
 std::cout << "2) Starting ... " << std::flush;

 ba::system_timer timer{ io, sc::seconds(1) };  // 2
 std::cout << "hello " << std::flush;;  // 3
 std::cout << "done" << std::endl;
1. The old io_service is now deprecated, get used to see io_context in its place.
2. Create a system timer on ASIO, setting its expire time to one second. Then start a non-blocking wait on it, passing the address of above defined hello() function as parameter.
3. After doing some job on the current thread, here just a print on the output console, we want to wait for the termiantion of the other thread, controlled by ASIO. So we call its run() method, blocking until we get back the control.

More action

Let's add some meat to the above example. Say that we want to run a loop in the main thread until a timeout expires.
class MyJob
 MyJob(const MyJob&) = delete;  // 1
 const MyJob& operator=(const MyJob&) = delete;

 std::mutex mx_;  // 2
 std::atomic<bool> expired_;  // 3
 MyJob() : expired_(false) {}

 void log(const char* message)
  std::unique_lock<std::mutex> lock(mx_);  // 4
  std::cout << message << std::flush;

 void timeout()  // 5
  expired_ = true;

 void operator()()  // 6
  for (int i = 0; !expired_; ++i)
   std::ostringstream os;
   os << '[' << i << ']';

1. The objects of this class are inherently non-copyable. So I remove copy ctor and assignment operator from its interface.
2. The output console is shared between two threads. This mutex is going to rule its access.
3. The threads are going to synchronize on a boolean. At startup it is set to false. When the timer expires set it to true. The main loop runs until it sees an expiration. Being just one thread setting it, while the other only reading it, an atomic boolean would be enough to ensure communication between threads.
4. Lock the mutex to acquire exclusive access to the shared resource.
5. This method is going to be called by the timer on expiration.
6. The loop I want to run until timer expiration.

Let's see how I used this class.
void timer2a(ba::io_context& io)
 std::cout << "2a) Starting ... " << std::flush;

 ba::system_timer timer(io, sc::seconds(1));

 MyJob job;
 timer.async_wait([&job](const bs::error_code&) { job.timeout(); });  // 1
 std::thread thread(std::ref(job));  // 2;
1. The async_wait() method of the timer is fed with a lambda that capture by reference the instance of the class MyJob that I created in the previous line. In the lambda body we call the job timeout() method. The result is that the expiration flag is set when the timeout expires. Here I ignore the ASIO error code, but it would be easy to take notice of it, when required.
2. Create a new thread to run the job loop.

One thing more. I want to run the two examples, one after the other, using the same ASIO io_context object. But the first one run() it, putting it in the stopped status. No problem, I just have to remember to reset it. That is what I do in the main:


I have pushed both C++ source files on GitHub, the simpler, and the more interesting one.

No comments:

Post a Comment