Подтвердить что ты не робот

Когда использовать `asio_handler_invoke`?

Вопрос

Когда нужно использовать asio_handler_invoke для достижения чего-то, чего нельзя сделать, просто обернув обработчик?

Канонический пример, демонстрирующий случай, когда требуется asio_handler_invoke, был бы идеальным.

Фон

В документах boost asio содержатся примеры использования asio_handler_invoke здесь, но я не думаю, что это убедительный пример почему вы будете использовать обработчик вызова. В этом примере вы можете сделать такие изменения, как следующие (и удалить asio_handler_invoke), и получить идентичный результат:

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

Аналогично, в моем ответе, относящемся к отслеживанию обработчика, аналогичным образом не нужно использовать asio_handler_invoke, несмотря на Tanner Sansbury's, предлагая использовать крючок вызова в качестве решения.

Этот поток в группе пользователей boost предоставляет дополнительную информацию - но я не понимаю значения.

Из того, что я видел, кажется, что asio_handler_invoke всегда называется как asio_handler_invoke(h, &h), что, похоже, не имеет большого смысла. В каких случаях аргументы не были (по существу) копиями одного и того же объекта?

Конечная точка - я вызываю только io_service::run() только из одного потока, так что может быть, я пропускаю что-то очевидное, исходящее из опыта в многопоточном цикле.

4b9b3361

Ответ 1

Короче говоря, обертка обработчика и asio_handler_invoke выполняет две разные задачи:

  • оберните обработчик, чтобы настроить вызов обработчика.
  • define asio_handler_invoke hook для настройки вызова других обработчиков в контексте обработчика.
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);

Основной причиной крюка asio_handler_invoke является возможность настроить стратегию вызова обработчиков, к которой приложение может иметь прямой доступ. Например, рассмотрите сконфигурированные операции, которые состоят из нуля или более вызовов промежуточных операций. Для каждой промежуточной операции промежуточный обработчик будет создан от имени приложения, но приложение не имеет прямого доступа к этим обработчикам. При использовании пользовательских обработчиков крюк asio_handler_invoke обеспечивает способ настройки стратегии вызова этих промежуточных обработчиков в заданном контексте. В документации указано:

Когда асинхронные операции состоят из других асинхронных операций, все промежуточные обработчики должны вызываться с использованием того же метода, что и конечный обработчик. Это необходимо для обеспечения того, чтобы пользовательские объекты не обращались таким образом, чтобы они могли нарушать гарантии. Эта функция [ asio_handler_invoke] гарантирует, что вызываемый метод, используемый для конечного обработчика, доступен на каждом промежуточном этапе.


asio_handler_invoke

Рассмотрим случай, когда мы хотим подсчитать количество выполняемых асинхронных операций, включая каждую промежуточную операцию в сложенных операциях. Для этого нам нужно создать собственный тип обработчика, counting_handler и посчитать количество раз, когда функции вызываются в его контексте:

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);

В приведенном выше фрагменте функция handle_read завершается counting_handler. Поскольку counting_handler не интересуется подсчетом количества раз, когда вызывается обработанный обработчик, его operator() не увеличивает счетчик и просто вызывает handle_read. Тем не менее, counting_handler интересуется количеством обработчиков, вызываемых в его контексте в операции async_read, поэтому стратегия пользовательского вызова в asio_handler_invoke будет увеличивать счет.


Пример

Вот конкретный пример, основанный на указанном выше типе counting_handler. Класс operation_counter предоставляет возможность легко переносить обработчики приложений с помощью 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;

Операция async_read() будет реализована в нуле или более промежуточном stream.async_read_some(). Для каждой из этих промежуточных операций будет создан и активирован обработчик с неопределенным типом. Если вышеуказанная операция async_read() была реализована в терминах 2 промежуточных async_read_some() операций, то counter.count() будет 2, а обработчик, возвращенный из counter.wrap(), будет вызван один раз.

С другой стороны, если бы кто-то не предоставлял крюк asio_handler_invoke и вместо этого увеличивал счетчик только в завершенном вызове обработчика, тогда счетчик был бы 1, отражающий только количество раз, которое обернутый обработчик был вызывается:

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.
};

Вот полный пример демонстрирующий подсчет количества выполняемых асинхронных операций, включая промежуточные операции из сложенной операции. В примере только три асинхронные операции (async_accept, async_connect и async_read), но операция async_read будет состоять из 2 промежуточных async_read_some операций:

#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;
}

Вывод:

total operations: 4
read operations: 2

Обработанные обработчики

В приведенном выше примере обработчик async_read() состоял из обработчика, завернутого дважды. Сначала с помощью operation_counter, который учитывает только операции чтения, а затем полученный функтор был обернут operation_counter подсчетом всех операций:

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

Реализация counting_handler asio_handler_invoke записывается для поддержки композиции путем вызова функции в контексте обернутого контекста обработчика. Это приводит к соответствующему подсчету для каждого 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_);
}

С другой стороны, если asio_handler_invoke явно называется function(), тогда будет задействована только внешняя стратегия вызова оболочки. В этом случае это приведет к тому, что all_operations.count() будет 4 и read_only.count() равен 0:

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

При создании обработчиков помните, что вызываемый asio_handler_invoke hook, который вызывается, находится через зависящий от аргумента поиск, поэтому он основан на точном типе обработчика. Составляющие обработчики с типами, не знающими asio_handler_invoke, будут препятствовать цепочки стратегий вызова. Например, использование std::bind() или std::function приведет к вызову по умолчанию asio_handler_invoke, в результате чего будут вызываться стратегии пользовательских вызовов:

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

Правильные стратегии вызова цепочек для составленных обработчиков могут быть очень важными. Например, тип неуказанного обработчика, возвращенный из strand.wrap(), обеспечивает гарантию того, что начальный обработчик, завернутый строкой и функциями, вызванными в контексте возвращенного обработчик не будет запускаться одновременно. Это позволяет удовлетворить требования безопасности потоков для многих объектов ввода-вывода при использовании скомпонованных операций, поскольку strand может использоваться для синхронизации с этими промежуточными операциями, к которым приложение не имеет доступа.

При запуске io_service по нескольким потокам нижеприведенный фрагмент кода может вызывать поведение undefined, поскольку промежуточные операции для обеих сконфигурированных операций могут выполняться одновременно, поскольку std::bind() не будет вызывать соответствующий 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)));