Фонд
Почнемо зі спрощеного прикладу та розглянемо відповідні фрагменти 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);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
Що таке обробник ?
Оброблювач не більше ніж на зворотний виклик. У прикладі коду є 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:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
На високому рівні програма створить 2 потоки, які оброблятимуть io_service
цикл подій (2). Це призводить до простого пулу потоків, який обчислює числа Фібоначчі (3).
Основна різниця між Кодом запитань і цим кодом полягає в тому, що цей код викликає io_service::run()
(2) до фактичної роботи та обробників, доданих до io_service
(3). Щоб запобігти io_service::run()
негайному поверненню, створюється io_service::work
об’єкт (1). Цей об'єкт запобігає закінченню io_service
роботи; отже, io_service::run()
не повернеться в результаті ніякої роботи.
Загальний потік такий:
- Створіть і додайте
io_service::work
об'єкт, доданий до io_service
.
- Створено пул потоків, який викликає
io_service::run()
. Ці робочі потоки не повернуться з- io_service
за io_service::work
об'єкта.
- Додайте до обробника 3, які обчислюють числа Фібоначчі
io_service
, і негайно поверніть. Робочі потоки, а не основний потік, можуть негайно почати запускати ці обробники.
- Видалити
io_service::work
об’єкт.
- Зачекайте, поки робочі потоки закінчаться. Це відбуватиметься лише після того, як всі 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));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
Синхронний проти асинхронного
Хоча в коді, про який йде мова, використовується асинхронна операція, він ефективно функціонує синхронно, оскільки чекає завершення асинхронної операції:
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 .