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

Io_service:: poll_one не детерминированное поведение

В следующем коде я ожидаю, что вывод всегда будет 1, потому что я ожидаю, что только один обработчик будет запущен при вызове poll_one(). Однако, примерно в 300 раз, выход на самом деле 3. Основываясь на моем понимании библиотеки boost, это кажется неправильным. Является ли неопределенное поведение ошибкой или ожидаемым?

#include <boost/asio.hpp>

int main() {
  boost::asio::io_service io;
  boost::asio::io_service::work io_work(io);
  boost::asio::io_service::strand strand1(io);
  boost::asio::io_service::strand strand2(io);
  int val = 0;

  strand1.post([&val, &strand2]() {
    val = 1;
    strand2.post([&val]() {
      val = 2;
    });
    boost::asio::spawn(strand2, [&val](boost::asio::yield_context yield) {
      val = 3;
    });
  });

  io.poll_one();
  std::cout << "Last executed: " << val << std::endl;

  return 0;
}

Использование boost-asio 1.60.0.6

4b9b3361

Ответ 1

Наблюдаемое поведение хорошо определено и ожидается, но не следует ожидать, что оно произойдет часто.

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

#include <cassert>
#include <boost/asio.hpp>

int main()
{
  boost::asio::io_service io_service;
  boost::asio::io_service::strand strand1(io_service);
  // Have strand2 use the same implementation as strand1.
  boost::asio::io_service::strand strand2(strand1);

  int value = 0;
  auto handler1 = [&value, &strand1, &strand2]() {
    assert(strand1.running_in_this_thread());
    assert(strand2.running_in_this_thread());
    value = 1;

    // handler2 is queued into strand and never invoked.
    auto handler2 = [&value]() { assert(false); };
    strand2.post(handler2);

    // handler3 is immediately executed.
    auto handler3 = [&value]() { value = 3; };
    strand2.dispatch(handler3);
    assert(value == 3);
  };

  // Enqueue handler1.
  strand1.post(handler1);

  // Run the event processing loop, executing handler1.
  assert(io_service.poll_one() == 1);
}

В приведенном выше примере:

  • io_service.poll_one() выполняет один готовый обработчик (handler1)
  • handler2 никогда не вызывается
  • handler3 вызывается непосредственно в strand2.dispatch(), поскольку strand2.dispatch() вызывается из обработчика, где strand2.running_in_this_thread() возвращает true

Существуют различные детали, способствующие наблюдаемому поведению:

  • io_service::poll_one() будет запускать цикл событий io_service и без блокировки, он будет выполнять не более одного готового к запуску обработчика. Обработчики, выполняемые непосредственно в контексте dispatch(), никогда не помещаются в очередь io_service и не подлежат ограничению poll_one() вызова одного обработчика.

  • Перегрузка boost::asio::spawn(strand, function) запускает стековый сопроцессор as-if через strand.dispatch():

    • Если strand.running_in_this_thread() возвращает false для вызывающего, тогда сопрограмма будет отправлена ​​в strand для отложенного вызова
    • если strand.running_in_this_thread() возвращает true для вызывающего, тогда сопрограмма будет выполнена немедленно
  • Дискретные объекты strand, которые используют одну и ту же реализацию, все еще сохраняют гарантии нити. А именно, одновременное выполнение не будет происходить, и порядок обращения обработчика будет определен. Когда дискретные объекты strand используют дискретные реализации, а несколько потоков запускают io_service, тогда можно наблюдать одновременное выполнение отдельных цепей. Однако, когда дискретные объекты strand используют одну и ту же реализацию, не будут наблюдаться concurrency, даже если несколько потоков работают с io_service. Это поведение задокументировано:

    Реализация не гарантирует, что обработчики, отправленные или отправленные через разные объекты strand, будут вызываться одновременно.

  • Asio имеет ограниченный набор реализаций строк. Текущее значение по умолчанию - 193, и его можно контролировать, определяя BOOST_ASIO_STRAND_IMPLEMENTATIONS на нужный номер. Эта функция отмечена в примечаниях к выпуску Boost.Asio 1.48

    Изменилось количество реализаций строк, задав BOOST_ASIO_STRAND_IMPLEMENTATIONS на нужный номер.

    Уменьшая размер пула, вы увеличиваете вероятность того, что две дискретные нити будут использовать одну и ту же реализацию. Если исходный код должен был установить размер пула 1, то strand1 и strand2 всегда будут использовать ту же реализацию, в результате val всегда будет 3 (демо).

  • Стратегия по умолчанию для реализации реализаций строк заключается в использовании хеша с золотым соотношением. Поскольку используется алгоритм хэширования, существует потенциал для коллизий, в результате чего одна и та же реализация используется для нескольких дискретных объектов strand. Определив BOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION, можно изменить стратегию распределения на раунд-робин, предотвращая возникновение конфликта до тех пор, пока не произойдет выделение строк BOOST_ASIO_STRAND_IMPLEMENTATIONS + 1. Эта функция отмечена в примечаниях к выпуску Boost.Asio 1.48:

    Добавлена ​​поддержка нового флага BOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION, который переключает распределение реализаций строк для использования циклического подхода, а не хеширования.

Учитывая приведенные выше детали, происходит следующее, когда 1 наблюдается в исходном коде:

  • strand1 и strand2 имеют дискретные реализации
  • io_service::poll_one() выполняет одиночный обработчик, который был отправлен непосредственно в strand1
  • обработчик, который был отправлен в strand1, устанавливает val в 1
  • обработчик, отправленный в strand2, находится в очереди и никогда не вызывается
  • создание сопрограммы отложено, так как strand порядок гарантии обращения предотвращает создание сопрограммы, пока не выполнит предыдущий обработчик, который был отправлен в strand2:

    задан объект strand s, если s.post(a) произойдет до s.dispatch(b), где последний выполняется за пределами строки, тогда asio_handler_invoke(a1, &a1) произойдет до asio_handler_invoke(b1, &b1).

С другой стороны, когда 3 наблюдается:

  • происходит хеш-столкновение для strand1 и strand2, в результате чего они используют ту же реализацию базовой строки
  • io_service::poll_one() выполняет одиночный обработчик, который был отправлен непосредственно в strand1
  • обработчик, который был отправлен в strand1, устанавливает val в 1
  • обработчик, отправленный в strand2, находится в очереди и никогда не вызывается
  • coroutine немедленно создается и вызывается внутри boost::asio::spawn(), устанавливая val в 3, так как strand2 может безопасно выполнять сопрограмму, сохраняя при этом гарантию неконкурентного выполнения и порядка вызова обработчика