Плутанина, коли метод boost :: asio :: io_service запускає / розблоковує метод запуску


88

Будучи початківцем Boost.Asio, мене бентежить io_service::run(). Буду вдячний, якщо хтось може пояснити мені, коли цей метод блокує / розблокує. У документації зазначено:

Ці run()функціональні блоки , поки вся робота не буде закінчена , і більше немає оброблювачів бути посланим, або до тих пір , io_serviceпоки не припинилося.

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

Звичайний вихід із run()функції означає, що io_serviceоб’єкт зупинено ( stopped()функція повертає true). Наступні виклики run(), run_one(), poll()або poll_one()відразу ж повертаються , якщо немає попереднього виклик reset().

Що означає наступне твердження?

[...] більше відправників не буде відправлено [...]


Намагаючись зрозуміти поведінку io_service::run(), я натрапив на цей приклад (приклад 3а). Всередині цього я спостерігаю, що io_service->run()блокує і чекає на замовлення на роботу.

// WorkerThread invines io_service->run()
void WorkerThread(boost::shared_ptr<boost::asio::io_service> io_service);
void CalculateFib(size_t);

boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service::work> work(
   new boost::asio::io_service::work(*io_service));

// ...

boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
  worker_threads.create_thread(boost::bind(&WorkerThread, io_service));
}

io_service->post( boost::bind(CalculateFib, 3));
io_service->post( boost::bind(CalculateFib, 4));
io_service->post( boost::bind(CalculateFib, 5));

work.reset();
worker_threads.join_all();

Однак у наступному коді, над яким я працював, клієнт підключається за допомогою TCP / IP і блоку методу запуску, поки дані не будуть отримані асинхронно.

typedef boost::asio::ip::tcp tcp;
boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<tcp::socket> socket(new tcp::socket(*io_service));

// Connect to 127.0.0.1:9100.
tcp::resolver resolver(*io_service);
tcp::resolver::query query("127.0.0.1", 
                           boost::lexical_cast< std::string >(9100));
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
socket->connect(endpoint_iterator->endpoint());

// Just blocks here until a message is received.
socket->async_receive(boost::asio::buffer(buf_client, 3000), 0,
                      ClientReceiveEvent);
io_service->run();

// Write response.
boost::system::error_code ignored_error;
std::cout << "Sending message \n";
boost::asio::write(*socket, boost::asio::buffer("some data"), ignored_error);

run()Буде вдячне будь-яке пояснення, яке описує його поведінку в двох прикладах нижче.

Відповіді:


234

Фонд

Почнемо зі спрощеного прикладу та розглянемо відповідні фрагменти Boost.Asio:

void handle_async_receive(...) { ... }
void print() { ... }

...  

boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);

...

io_service.post(&print);                             // 1
socket.connect(endpoint);                            // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print);                             // 4
io_service.run();                                    // 5

Що таке обробник ?

Оброблювач не більше ніж на зворотний виклик. У прикладі коду є 3 обробники:

  • printОброблювач (1).
  • handle_async_receiveОброблювач (3).
  • printОброблювач (4).

Незважаючи на те, що одна і та ж print()функція використовується двічі, кожне використання розглядається для створення власного обробника, який можна ідентифікувати однозначно. Обробники можуть бути різних форм і розмірів, починаючи від базових функцій, як описані вище, і закінчуючи більш складними конструкціями, такими як функтори, що генеруються з boost::bind()та лямбда. Незалежно від складності, обробник залишається не більше ніж зворотним дзвінком.

Що таке робота ?

Робота - це деяка обробка, яку Boost.Asio просили виконати від імені коду програми. Іноді Boost.Asio може розпочати частину роботи, як тільки про це повідомляють, а інший раз може зачекати, щоб виконати роботу пізніше. Після завершення роботи Boost.Asio повідомить програму, викликаючи наданий обробник .

Boost.Asio гарантує , що обробники будуть працювати тільки в потоці , який в даний час викликає run(), run_one(), poll()або poll_one(). Це потоки, які будуть виконувати роботу та обробляти виклики . Отже, у наведеному вище прикладі print()не викликається, коли його розміщують у io_service(1). Натомість він додається до io_serviceі буде викликаний пізніше. У цьому випадку це в межах io_service.run()(5).

Що таке асинхронні операції?

Асинхронна операція створює роботу і Boost.Asio викликатиме обробник інформувати додаток , коли робота завершена. Асинхронні операції створюються за допомогою виклику функції, що має ім'я з префіксом async_. Ці функції також відомі як ініціюючі функції .

Асинхронні операції можна розкласти на три унікальні етапи:

  • Ініціювання або інформування пов'язаних з io_serviceцим робіт, які потрібно виконати. async_receiveОперації (3) повідомляє , io_serviceщо вона буде необхідно асинхронно зчитувати дані з гнізда, а потім async_receiveповертається негайно.
  • Виконання фактичної роботи. У цьому випадку при отриманні socketданих байти будуть прочитані та скопійовані buffer. Фактична робота буде виконана в будь-якому:
    • Ініціююча функція (3), якщо Boost.Asio може визначити, що вона не заблокує.
    • Коли програма явно запускає io_service(5).
  • Виклик handle_async_receive ReadHandler . Знову ж таки, обробники викликаються лише в потоках, що запускають io_service. Таким чином, незалежно від того, коли робота виконана (3 або 5), гарантовано, що handle_async_receive()буде викликано лише протягом io_service.run()(5).

Поділ у часі та просторі між цими трьома ступенями відоме як інверсія управління потоком. Це одна зі складнощів, яка ускладнює асинхронне програмування. Однак існують методи, які можуть допомогти пом'якшити це, наприклад, використання спільних програм .

Що робить io_service.run()?

Коли потік викликає io_service.run(), робота та обробники будуть викликані з цього потоку. У наведеному вище прикладі io_service.run()(5) буде блокувати, поки:

  • Він викликав і повернувся від обох printобробників, операція отримання завершується з успіхом або невдачею, а його handle_async_receiveобробник викликається і повертається.
  • io_serviceЯвно зупинено через io_service::stop().
  • Виняток видається зсередини обробника.

Один потенційний потік пседо-іш можна описати наступним чином:

створити io_service
створити сокет
додати обробник друку до io_service (1)
дочекайтеся підключення розетки (2)
додати асинхронний робочий запит на читання до io_service (3)
додати обробник друку до io_service (4)
запустити io_service (5)
  є робота чи обробники?
    так, є 1 робота та 2 обробники
      чи сокет має дані? ні, нічого не робити
      запустити обробник друку (1)
  є робота чи обробники?
    так, є 1 робота та 1 обробник
      чи сокет має дані? ні, нічого не робити
      запустити обробник друку (4)
  є робота чи обробники?
    так, є 1 робота
      чи сокет має дані? ні, продовжуйте чекати
  - сокет приймає дані -
      socket має дані, зчитай їх у буфер
      додати обробник handle_async_receive до io_service
  є робота чи обробники?
    так, є 1 обробник
      запустити обробник handle_async_receive (3)
  є робота чи обробники?
    ні, встановіть io_service як зупинене і поверніть

Зверніть увагу, що коли читання закінчилося, воно додало ще один обробник до io_service. Ця тонка деталь є важливою особливістю асинхронного програмування. Це дозволяє ланцюгам обробників . Наприклад, якби handle_async_receiveне отримали всі очікувані дані, то його реалізація могла б опублікувати ще одну асинхронну операцію зчитування, що призвело б до io_serviceбільшої роботи і, отже, повернення з io_service.run().

Зверніть увагу, що коли програма io_serviceзакінчила роботу, програма повинна перед reset()тим, io_serviceяк запустити її знову.


Приклад запитання та приклад 3а коду

Тепер давайте розглянемо дві частини коду, на які посилається запитання.

Код питання

socket->async_receiveдодає роботу до io_service. Таким чином, io_service->run()буде блокувати, поки операція зчитування не завершиться з успіхом або помилкою, ClientReceiveEventабо завершить роботу або не видасть виключення.

Приклад 3a Код

У надії полегшити розуміння, ось менший анотований Приклад 3a:

void CalculateFib(std::size_t n);

int main()
{
  boost::asio::io_service io_service;
  boost::optional<boost::asio::io_service::work> work =       // '. 1
      boost::in_place(boost::ref(io_service));                // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'

  io_service.post(boost::bind(CalculateFib, 3));              // '.
  io_service.post(boost::bind(CalculateFib, 4));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  work = boost::none;                                         // 4
  worker_threads.join_all();                                  // 5
}

На високому рівні програма створить 2 потоки, які оброблятимуть io_serviceцикл подій (2). Це призводить до простого пулу потоків, який обчислює числа Фібоначчі (3).

Основна різниця між Кодом запитань і цим кодом полягає в тому, що цей код викликає io_service::run()(2) до фактичної роботи та обробників, доданих до io_service(3). Щоб запобігти io_service::run()негайному поверненню, створюється io_service::workоб’єкт (1). Цей об'єкт запобігає закінченню io_serviceроботи; отже, io_service::run()не повернеться в результаті ніякої роботи.

Загальний потік такий:

  1. Створіть і додайте io_service::workоб'єкт, доданий до io_service.
  2. Створено пул потоків, який викликає io_service::run(). Ці робочі потоки не повернуться з- io_serviceза io_service::workоб'єкта.
  3. Додайте до обробника 3, які обчислюють числа Фібоначчі io_service, і негайно поверніть. Робочі потоки, а не основний потік, можуть негайно почати запускати ці обробники.
  4. Видалити io_service::workоб’єкт.
  5. Зачекайте, поки робочі потоки закінчаться. Це відбуватиметься лише після того, як всі 3 обробники закінчать виконання, оскільки io_serviceжоден з них не має обробників і не працює.

Код може бути написаний по-різному, таким же чином, як оригінальний код, де обробники додаються до io_service, а потім io_serviceобробляється цикл подій. Це позбавляє від необхідності використовувати io_service::workі призводить до отримання наступного коду:

int main()
{
  boost::asio::io_service io_service;

  io_service.post(boost::bind(CalculateFib, 3));              // '.
  io_service.post(boost::bind(CalculateFib, 4));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'
  worker_threads.join_all();                                  // 5
}

Синхронний проти асинхронного

Хоча в коді, про який йде мова, використовується асинхронна операція, він ефективно функціонує синхронно, оскільки чекає завершення асинхронної операції:

socket.async_receive(buffer, handler)
io_service.run();

еквівалентно:

boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);

Як загальне правило, намагайтеся уникати поєднання синхронних та асинхронних операцій. Часто це може перетворити складну систему на складну. Ця відповідь підкреслює переваги асинхронного програмування, деякі з яких також висвітлено в документації Boost.Asio .


13
Дивовижний пост. Я хотів би додати лише одну річ, тому що, на мою думку, йому приділяється недостатньо уваги: ​​Після повернення run () вам потрібно зателефонувати до reset () на вашому io_service, перш ніж ви зможете запустити () знову. В іншому випадку він може миттєво повернутися незалежно від того, чи чекають операції async_ чи ні.
DeVadder

Звідки береться буфер? Що це?
ruipacheco

Я все ще розгублений. Якщо змішування є синхронізацією, а асинхронізація не рекомендується, то що таке режим чистої асинхронізації? ви можете навести приклад, що показує код без io_service.run () ;?
Сплеск

@Splash Можна використовувати io_service.poll()для обробки циклу подій, не блокуючи видатних операцій. Основною рекомендацією уникати змішування синхронних та асинхронних операцій є уникнення додавання зайвої складності та запобігання поганій реакції, коли обробники займають тривалий час. Є деякі випадки, коли це безпечно, наприклад, коли відомо, що синхронна операція не блокується.
Таннер Сансбері

Що ви маєте на увазі під словомrun() "на даний момент" у "Boost.Asio гарантує, що обробники працюватимуть лише в потоці, який наразі викликає ...." ? Якщо існує N потоків (який викликав run()), то який із них є "поточним"? Може бути багато? Або ви маєте на увазі, що потік, який закінчив виконувати async_*()(скажімо async_read), гарантовано також викликає свої обробники?
Nawaz

18

Для спрощення того, що runробить, подумайте про це як про працівника, який повинен обробити купу паперу; він бере один аркуш, робить те, що розповідає аркуш, викидає аркуш і бере наступний; коли у нього закінчуються аркуші, він залишає кабінет. На кожному аркуші може бути будь-яка інструкція, навіть додавання нового листа до купи. Повернутися до ASIO: ви можете дати до io_serviceроботи в двох напрямках, по суті: при використанні postна ньому , як у зразку ви пов'язані, або за допомогою інших об'єктів , які внутрішньо називають postна io_service, як socketі його async_*методи.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.