Zero Zero - 3 months ago 4
C++ Question

When to use `asio_handler_invoke`?

Question

When is it necessary to use

asio_handler_invoke
to achieve something that cannot be done by simply wrapping the handler?

A canonical example that demonstrates a case where
asio_handler_invoke
is required would be ideal.

Background

The boost asio docs contain an example of how to use
asio_handler_invoke
here, but I don't think it is a compelling example of why you would use the invocation handler. In that example it appears you could make changes like the following (and remove the
asio_handler_invoke
) and achieve an identical result:

template <typename Arg1>
void operator()(Arg1 arg1)
{
queue_.add(priority_, std::bind(handler_, arg1));
}


Similarly, in my answer relating to handler tracking it similarly appears unnecessary to use
asio_handler_invoke
, despite Tanner Sansbury's answer suggesting using the invocation hook as a solution.

This thread on the boost user group provides some more information - but I don't understand the significance.

From what I have seen, it appears
asio_handler_invoke
is always called like
asio_handler_invoke(h, &h)
, which doesn't appear to make much sense. In what cases would the arguments not be (essentially) copies of the same object?

A final point - I only ever call
io_service::run()
from a single thread, so it may be I'm missing something obvious that comes from experience in a multi-threaded loop.

Answer

In short, wrapping a handler and asio_handler_invoke accomplish two different tasks:

  • wrap a handler to customize the invocation of a handler.
  • define asio_handler_invoke hook to customize the invocation of other handlers in the context of a handler.
template <typename Handler>
struct custom_handler
{
  void operator()(...); // Customize invocation of handler_.
  Handler handler_;
};

// Customize invocation of Function within context of custom_handler.
template <typename Function>
void asio_handler_invoke(Function function, custom_handler* context);

// Invoke custom invocation of 'perform' within context of custom_handler.
void perform() {...}
custom_handler handler;
using boost::asio::asio_handler_invoke;
asio_handler_invoke(std::bind(&perform), &handler);

The primary reason for the asio_handler_invoke hook is to allow one to customize the invocation strategy of handlers to which the application may not have direct access. For instance, consider composed operations that are composed of zero or more calls to intermediate operations. For each intermediate operation, an intermediate handler will be created on behalf of the application, but the application does not have direct access to these handlers. When using custom handlers, the asio_handler_invoke hook provides a way to customize the invocation strategy of these intermediate handlers within a given context. The documentation states:

When asynchronous operations are composed from other asynchronous operations, all intermediate handlers should be invoked using the same method as the final handler. This is required to ensure that user-defined objects are not accessed in a way that may violate the guarantees. This [asio_handler_invoke] hooking function ensures that the invoked method used for the final handler is accessible at each intermediate step.


asio_handler_invoke

Consider a case where we wish to count the number of asynchronous operations executed, including each intermediate operation in composed operations. To do this, we need to create a custom handler type, counting_handler, and count the number of times functions are invoked within its context:

template <typename Handler>
class counting_handler
{
  void operator()(...)
  {
    // invoke handler
  } 
  Handler handler_;
};

template <typename Function>
void asio_handler_invoke(Function function, counting_handler* context)
{
  // increment counter
  // invoke function
}

counting_handler handler(&handle_read);
boost::asio::async_read(socket, buffer, handler);

In the above snippet, the function handle_read is wrapped by counting_handler. As the counting_handler is not interested in counting the number of times the wrapped handler is invoked, its operator() will not increment the count and just invoke handle_read. However, the counting_handler is interested in the amount of handlers invoked within its context in the async_read operation, so the custom invocation strategy in asio_handler_invoke will increment a count.


Example

Here is a concrete example based on the above counting_handler type. The operation_counter class provides a way to easily wrap application handlers with a counting_handler:

namespace detail {

/// @brief counting_handler is a handler that counts the number of
///        times a handler is invoked within its context.
template <class Handler>
class counting_handler
{
public:
  counting_handler(Handler handler, std::size_t& count)
    : handler_(handler),
      count_(count)
  {}

  template <class... Args>
  void operator()(Args&&... args)
  {
    handler_(std::forward<Args>(args)...);
  }

  template <typename Function>
  friend void asio_handler_invoke(
    Function intermediate_handler,
    counting_handler* my_handler)
  {
    ++my_handler->count_;
    // Support chaining custom strategies incase the wrapped handler
    // has a custom strategy of its own.
    using boost::asio::asio_handler_invoke;
    asio_handler_invoke(intermediate_handler, &my_handler->handler_);
  }

private:
  Handler handler_;
  std::size_t& count_;
};

} // namespace detail

/// @brief Auxiliary class used to wrap handlers that will count
///        the number of functions invoked in their context.
class operation_counter
{
public:

  template <class Handler>
  detail::counting_handler<Handler> wrap(Handler handler)
  {
    return detail::counting_handler<Handler>(handler, count_);
  }

  std::size_t count() { return count_; }

private:
  std::size_t count_ = 0;
};

...

operation_counter counter;
boost::asio::async_read(socket, buffer, counter.wrap(&handle_read));
io_service.run();
std::cout << "Count of async_read_some operations: " <<
             counter.count() << std::endl;

The async_read() composed operation will be implemented in zero or more intermediate stream.async_read_some() operations. For each of these intermediate operations, a handler with an unspecified type will be created and invoked. If the above async_read() operation was implemented in terms of 2 intermediate async_read_some() operations, then counter.count() will be 2, and the handler returned from counter.wrap() got invoked once.

On the other hand, if one were to not provide an asio_handler_invoke hook and instead only incremented the count within the wrapped handler's invocation, then the count would be 1, reflecting only the count of times the wrapped handler was invoked:

template <class Handler>
class counting_handler
{
public:
  ...

  template <class... Args>
  void operator()(Args&&... args)
  {
    ++count_;
    handler_(std::forward<Args>(args)...);
  }

  // No asio_handler_invoke implemented.
};

Here is a complete example demonstrating counting the number of asynchronous operations that get executed, including intermediate operations from a composed operation. The example only initiates three async operations (async_accept, async_connect, and async_read), but the async_read operation will be composed of 2 intermediate async_read_some operations:

#include <functional> // std::bind
#include <iostream>   // std::cout, std::endl
#include <utility>    // std::forward
#include <boost/asio.hpp>

// This example is not interested in the handlers, so provide a noop function
// that will be passed to bind to meet the handler concept requirements.
void noop() {}

namespace detail {

/// @brief counting_handler is a handler that counts the number of
///        times a handler is invoked within its context.
template <class Handler>
class counting_handler
{
public:
  counting_handler(Handler handler, std::size_t& count)
    : handler_(handler),
      count_(count)
  {}

  template <class... Args>
  void operator()(Args&&... args)
  {
    handler_(std::forward<Args>(args)...);
  }

  template <typename Function>
  friend void asio_handler_invoke(
    Function function,
    counting_handler* context)
  {
    ++context->count_;
    // Support chaining custom strategies incase the wrapped handler
    // has a custom strategy of its own.
    using boost::asio::asio_handler_invoke;
    asio_handler_invoke(function, &context->handler_);
  }

private:
  Handler handler_;
  std::size_t& count_;
};

} // namespace detail

/// @brief Auxiliary class used to wrap handlers that will count
///        the number of functions invoked in their context.
class operation_counter
{
public:

  template <class Handler>
  detail::counting_handler<Handler> wrap(Handler handler)
  {
    return detail::counting_handler<Handler>(handler, count_);
  }

  std::size_t count() { return count_; }

private:
  std::size_t count_ = 0;
};

int main()
{
  using boost::asio::ip::tcp;
  operation_counter all_operations;

  // Create all I/O objects.
  boost::asio::io_service io_service;
  tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
  tcp::socket socket1(io_service);
  tcp::socket socket2(io_service);

  // Connect the sockets.
  // operation 1: acceptor.async_accept
  acceptor.async_accept(socket1, all_operations.wrap(std::bind(&noop)));
  // operation 2: socket2.async_connect
  socket2.async_connect(acceptor.local_endpoint(),
      all_operations.wrap(std::bind(&noop)));
  io_service.run();
  io_service.reset();

  // socket1 and socket2 are connected.  The scenario below will:
  // - write bytes to socket1.
  // - initiate a composed async_read operaiton to read more bytes
  //   than are currently available on socket2.  This will cause
  //   the operation to  complete with multple async_read_some 
  //   operations on socket2.
  // - write more bytes to socket1.

  // Write to socket1.
  std::string write_buffer = "demo";
  boost::asio::write(socket1, boost::asio::buffer(write_buffer));

  // Guarantee socket2 has received the data.
  assert(socket2.available() == write_buffer.size());

  // Initiate a composed operation to more data than is immediately
  // available.  As some data is available, an intermediate async_read_some
  // operation (operation 3) will be executed, and another async_read_some 
  // operation (operation 4) will eventually be initiated.
  std::vector<char> read_buffer(socket2.available() + 1);
  operation_counter read_only;
  boost::asio::async_read(socket2, boost::asio::buffer(read_buffer),
    all_operations.wrap(read_only.wrap(std::bind(&noop))));

  // Write more to socket1.  This will cause the async_read operation
  // to be complete.
  boost::asio::write(socket1, boost::asio::buffer(write_buffer));

  io_service.run();
  std::cout << "total operations: " << all_operations.count() << "\n"
               "read operations: " << read_only.count() << std::endl;
}

Output:

total operations: 4
read operations: 2

Composed Handlers

In the above example, the async_read() handler was composed of a handler wrapped twice. First by the operation_counter that is only counting for read operations, and then the resulting functor was wrapped by the operation_counter counting all operations:

boost::asio::async_read(..., all_operations.wrap(read_only.wrap(...)));

The counting_handler's asio_handler_invoke implementation is written to support composition by invoking the Function in the context of the wrapped handler's context. This results in the appropriate counting occurred for each operation_counter:

template <typename Function>
void asio_handler_invoke(
  Function function,
  counting_handler* context)
{
  ++context->count_;
  // Support chaining custom strategies incase the wrapped handler
  // has a custom strategy of its own.
  using boost::asio::asio_handler_invoke;
  asio_handler_invoke(function, &context->handler_);
}

On the other hand, if the asio_handler_invoke explicitly called function(), then only the outer most wrapper's invocation strategy would be invoked. In this case, it would result in all_operations.count() being 4 and read_only.count() being 0:

template <typename Function>
void asio_handler_invoke(
  Function function,
  counting_handler* context)
{
  ++context->count_;
  function(); // No chaining.
}

When composing handlers, be aware that the asio_handler_invoke hook that gets invoked is located through argument-dependent lookup, so it is based on the exact handler type. Composing handlers with types that are not asio_handler_invoke aware will prevent the chaining of invocation strategies. For instance, using std::bind() or std::function will result in the default asio_handler_invoke being called, causing custom invocation strategies from being invoked:

// Operations will not be counted.
boost::asio::async_read(..., std::bind(all_operations.wrap(...)));    

Proper chaining invocation strategies for composed handlers can be very important. For example, the unspecified handler type returned from strand.wrap() provides the guarantee that initial handler wrapped by the strand and functions invoked in the context of the returned handler will not run concurrently. This allows one to meet the thread-safety requirements of many of the I/O objects when using composed operations, as the strand can be used to synchronize with these intermediate operations to which the application does not have access.

When running the io_service by multiple threads, the below snippet may invoke undefined behavior, as the intermediate operations for both composed operations may run concurrently, as std::bind() will not invoke the appropriate asio_handler_hook:

boost::asio::async_read(socket, ..., std::bind(strand.wrap(&handle_read)));
boost::asio::async_write(socket, ..., std::bind(strand.wrap(&handle_write)));