Pages

Boost ASIO passing parameters to handler

I have sort of spoiled the argument of this post in the previous one, where the focus was on telling ASIO to asynchronously run a function when a timer expires, but I couldn't keep from presenting also a scenario where multiple threads synchronize on a flag and access concurrently a shared resource. Let's do a step beyond, and analyze a simpler case, where the function we want ASIO to run at the timer expiration just has to access a variable defined in the main thread.


Plain function

The point raised in the official Boost ASIO tutorial is having a simple function passed to ASIO so that after it is called the first time, on timer expirations, it resets the timer on itself, keeping track on the times it has been invoked in a counter defined in the main thread. Here is my version of it, with some minor variation.
namespace ba = boost::asio;
namespace sc = std::chrono;

// ...

void print(ba::system_timer* pTimer, int* pCount) {  // 1
 if (*pCount < 5)  // 2
 {
  std::cout << (*pCount)++ << ' ';
  pTimer->expires_at(pTimer->expiry() + sc::milliseconds(500));  // 3
  pTimer->async_wait(std::bind(print, pTimer, pCount));  // 4
 }
}
1. I use system_timer, based on the standard chrono::system_clock, instead of the boost::posix_time based deadline_timer.
2. The check on the counter is used to avoid an infinite series of calls.
3. Reset the timer expiration to half a second in the future. To get the current expiration time I use expiry() instead of the deprecated overaload with no parameters of expires_at().
4. Reset an asychronous wait on the timer, passing as parameter the function itself.

Notice how the standard bind() function is used to bind the print() function to the handler expected by async_wait() on the timer. This makes possible to elide the reference to boost::system::error_code, that we decided not to use here, and add instead the two parameters we actually need.

In the main thread we start the asychronous wait on the ASIO managed timer in this way:
// ...

int count = 0;
ba::system_timer timer(io, sc::milliseconds(500));  // 1

timer.async_wait(std::bind(print, &timer, &count));  // 2
io.run();  // 3
1. io is a boost::asio::io_context object - previously known as io_service.
2. Notice that both count and timer are shared between the main thread and the one owned by ASIO in which is going to be executed print().
3. However, nothing happens in the main thread until ASIO ends its run().

The code is so simple that we can guarantee it works fine. However, when between (2) and (3), another thread is spawned, with something involving io and timer (and cout, by the way) we should be ready to redesign the code for ensure thread safety.

Same, with lambda

Usually, I would feel more at ease putting the code above in a class, as I did in the previous post. Still, one could argue that in a simple case like this one, that could be kind of an overkill. Well, in this case I would probably go for a lambda implementation, that at least would keep the code close, making less probable forgetting something in case of future refactoring.

Since I could capture count and timer instead of passing them to the lambda, there is no need of custom binding here. However, the original function needs to use its name in its body, and this is something that C++ lambdas are not allowed to do. There are a couple of workaround available, I have chosen to save it as a local std::function variable, and then pass it to async_wait() like this:
// ...
std::function<void(const bs::error_code&)> print = [&](const bs::error_code&) {
 if (count < 5)
 {
  std::cout << count++ << ' ';
  timer.expires_at(timer.expiry() + sc::milliseconds(500));
  timer.async_wait(print);
 }
};

timer.async_wait(print);

I have pushed the two new files, free function and lambda example, on GitHub.

No comments:

Post a Comment