Зачем нужна строка для подключения при использовании boost:: asio?

Я просматриваю HTTP-сервер 3 на веб-сайте Boost.

Не могли бы вы объяснить, почему мне нужно strand за соединение? Как я вижу, мы называем read_some только в обработчике read-event. Таким образом, в основном вызовы read_some являются последовательными, поэтому нет необходимости в цепочке (и пункт 2 третьего абзаца говорит то же самое). Где риск в многопоточной среде?

Ответ 1

Документация верна. При реализации полудуплексного протокола, например HTTP Server 3, strand не требуется. Цепочки вызовов могут быть проиллюстрированы следующим образом:

void connection::start()
{
  socket.async_receive_from(..., &handle_read);  ----.
}                                                    |
    .------------------------------------------------'
    |      .-----------------------------------------.
    V      V                                         |
void connection::handle_read(...)                    |
{                                                    |
  if (result)                                        |
    boost::asio::async_write(..., &handle_write); ---|--.
  else if (!result)                                  |  |
    boost::asio::async_write(..., &handle_write);  --|--|
  else                                               |  |
    socket_.async_read_some(..., &handle_read);  ----'  |
}                                                       |
    .---------------------------------------------------'
    |
    V
void handle_write(...)

Как показано на иллюстрации, только одно асинхронное событие запускается на каждый путь. Без возможности одновременного выполнения обработчиков или операций на socket_ он, как говорят, работает в неявной цепочке.


Безопасность резьбы

Пока он не является проблемой в этом примере, я хотел бы выделить одну важную деталь строк и сконфигурированных операций, таких как boost::asio::async_write. Прежде чем объяснять детали, сначала включите модель безопасности потока с помощью Boost.Asio. Для большинства объектов Boost.Asio безопасно иметь несколько асинхронных операций, ожидающих обработки объекта; просто указано, что одновременные вызовы объекта небезопасны. На приведенных ниже диаграммах каждый столбец представляет поток, и каждая строка представляет, что поток делает в момент времени.

Безопасно, чтобы один поток выполнял последовательные вызовы, а другие потоки не выполняли:

 thread_1                             | thread_2
--------------------------------------+---------------------------------------
socket.async_receive(...);            | ...
socket.async_write_some(...);         | ...

Безопасно для нескольких потоков выполнять вызовы, но не одновременно:

 thread_1                             | thread_2
--------------------------------------+---------------------------------------
socket.async_receive(...);            | ...
...                                   | socket.async_write_some(...);

Однако для нескольких потоков небезопасно совершать вызовы одновременно 1:

 thread_1                             | thread_2
--------------------------------------+---------------------------------------
socket.async_receive(...);            | socket.async_write_some(...);
...                                   | ...

Пряди

Чтобы предотвратить одновременные вызовы, обработчики часто вызывается изнутри строк. Это делается либо:

  • Обертка обработчика с strand.wrap. Это вернет новый обработчик, который отправит через цепочку.
  • Проводка или отправка непосредственно через прядь.

Составленные операции уникальны в том, что промежуточные вызовы потока вызывают в цепочке обработчика, если он присутствует, а не в цепочке, в которой инициирована сгенерированная операция. По сравнению с другими операциями, это представляет собой инверсию, где указана прядь. Ниже приведен пример кода, ориентированного на использование строк, который будет демонстрировать сокет, который считывается с помощью несвязанной операции, и одновременно записывается с помощью сложенной операции.

void start()
{
  // Start read and write chains.  If multiple threads have called run on
  // the service, then they may be running concurrently.  To protect the
  // socket, use the strand.
  strand_.post(&read);
  strand_.post(&write);
}

// read always needs to be posted through the strand because it invokes a
// non-composed operation on the socket.
void read()
{
  // async_receive is initiated from within the strand.  The handler does
  // not affect the strand in which async_receive is executed.
  socket_.async_receive(read_buffer_, &handle_read);
}

// This is not running within a strand, as read did not wrap it.
void handle_read()
{
  // Need to post read into the strand, otherwise the async_receive would
  // not be safe.
  strand_.post(&read);
}

// The entry into the write loop needs to be posted through a strand.
// All intermediate handlers and the next iteration of the asynchronous write
// loop will be running in a strand due to the handler being wrapped.
void write()
{
  // async_write will make one or more calls to socket_.async_write_some.
  // All intermediate handlers (calls after the first), are executed
  // within the handler context (strand_).
  boost::asio::async_write(socket_, write_buffer_,
                           strand_.wrap(&handle_write));
}

// This will be invoked from within the strand, as it was a wrapped
// handler in write().
void handle_write()
{
  // handler_write() is invoked within a strand, so write() does not
  // have to dispatched through the strand.
  write();
}

Важность типов обработчиков

Кроме того, в составных операциях Boost.Asio использует зависимый от аргумента поиск (ADL) для вызова промежуточных обработчиков через цепочку обработчика завершения. Таким образом, важно, чтобы тип обработчика завершения имел соответствующие asio_handler_invoke() крючки. Если стирание типа происходит с типом, у которого нет соответствующих крючков asio_handler_invoke(), например, в случае, когда a boost::function создается из типа возврата strand.wrap, тогда промежуточные обработчики будут выполняться за пределами строки и только обработчик завершения будет выполняться внутри нити. Подробнее см. .

В следующем коде все промежуточные обработчики и обработчик завершения будут выполняться внутри строки:

boost::asio::async_write(stream, buffer, strand.wrap(&handle_write));

В следующем коде в строке будет выполняться только обработчик завершения. Ни один из промежуточных обработчиков не будет выполняться внутри строки:

boost::function<void()> handler(strand.wrap(&handle_write));
boost::asio::async_write(stream, buffer, handler);

<суб > 1. история изменений документирует аномалию этого правила. Если поддерживается ОС, операции synchronous для чтения, записи, принятия и подключения являются потокобезопасными. Я включаю его здесь для полноты, но предлагаю использовать его с осторожностью.

Ответ 2

Я считаю, что это потому, что сработала операция async_write. async_write состоит из нескольких socket:: async_write_some асинхронно. Strand помогает сериализовать эти операции. Крис Коххофф, автор асио, кратко рассказывает об этом в своем обсуждении около 1:17.