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

Попытка понять, как реализовать пользовательский сервис Boost.Asio

Я думаю о написании пользовательской службы Asio поверх существующего проприетарного сетевого протокола сторонних разработчиков, который мы сейчас используем.

В соответствии с Highscore Asio guide вам нужно реализовать три класса для создания пользовательской службы Asio:

  • Класс, полученный из boost::asio::basic_io_object, представляющий новый объект ввода-вывода.
  • Класс, полученный из boost::asio::io_service::service, представляющий сервис, который зарегистрирован в службе ввода-вывода, и к нему можно получить доступ из объекта ввода-вывода.
  • Класс, не полученный из любого другого класса, представляющего реализацию службы.

Реализация сетевого протокола уже предоставляет асинхронные операции и имеет (блокирующий) цикл событий. Поэтому я подумал, что я поместил бы его в класс реализации службы и запустил цикл событий во внутреннем потоке рабочего. Пока все хорошо.

Посмотрев на некоторые примеры пользовательских сервисов, я заметил, что классы обслуживания порождают собственные внутренние потоки (фактически они создают собственные внутренние экземпляры io_service). Например:

  • Страница Highscore предоставляет пример монитора каталога. Это, по сути, обертка вокруг inotify. Интересными классами являются inotify/basic_dir_monitor_service.hpp и inotify/dir_monitor_impl.hpp. Dir_monitor_impl обрабатывает фактическое взаимодействие с inofity, которое блокирует и, следовательно, работает в фоновом потоке. Я согласен с этим. Но basic_dir_monitor_service также имеет внутренний рабочий поток, и все, что, кажется, делает, это перетасовывать запросы между основными io_service и Dir_monitor_impl. Я играл с кодом, удалял рабочий поток в basic_dir_monitor_service и вместо этого отправлял запросы непосредственно в основной io_service, и программа по-прежнему выполнялась по-прежнему.

  • В Asio пользовательский пример службы журналов я заметил тот же подход. logger_service порождает внутренний рабочий поток для обработки запросов на ведение журнала. У меня не было времени, чтобы поиграть с этим кодом, но я думаю, что можно отправлять эти запросы непосредственно в основной io_service.

В чем преимущество наличия этих "посреднических работников"? Разве вы не могли постоянно публиковать всю работу в основном io_service? Пропустил ли я какой-то важный аспект шаблона Proactor?

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

4b9b3361

Ответ 1

Короче, последовательность. Службы пытаются удовлетворить ожидания пользователей, установленные службами Boost.Asio.

Использование внутреннего io_service обеспечивает четкое разделение владения и контроля обработчиков. Если пользовательская служба помещает свои внутренние обработчики в пользователя io_service, тогда выполнение внутренних обработчиков службы становится неявно связанным с обработчиками пользователей. Подумайте, как это повлияет на ожидания пользователей с помощью Boost.Asio Logger Service:

  • logger_service записывает в поток файлов в обработчике. Таким образом, программа, которая никогда не обрабатывает цикл событий io_service, такой как тот, который использует только синхронный API, никогда не будет записывать сообщения журнала.
  • logger_service больше не будет потокобезопасным, потенциально вызывающим поведение undefined, если io_service обрабатывается несколькими потоками.
  • Время жизни внутренних операций logger_service ограничено значением времени io_service. Например, когда вызывается функция service shutdown_service(), время жизни владельца io_service уже завершено. Следовательно, сообщения не могли регистрироваться через logger_service::log() внутри shutdown_service(), поскольку он попытался бы отправить внутренний обработчик в io_service, срок службы которого уже закончился.
  • Пользователь может больше не принимать сопоставление "один к одному" между операцией и обработчиком. Например:

    boost::asio::io_service io_service;
    debug_stream_socket socket(io_service);
    boost::asio::async_connect(socket, ..., &connect_handler);
    io_service.poll();
    // Can no longer assume connect_handler has been invoked.
    

    В этом случае io_service.poll() может вызывать обработчик, внутренний для logger_service, а не connect_handler().

Кроме того, эти внутренние потоки пытаются имитировать поведение, используемое внутренне с помощью Boost.Asio :

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


Пример монитора каталогов

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

Подробнее, когда асинхронная работа монитора инициируется через dir_monitor::async_monitor(), a basic_dir_monitor_service::monitor_operation отправляется во внутренний io_service. При вызове эта операция вызывает dir_monitor_impl::popfront_event(), потенциально блокирующий вызов. Следовательно, если monitor_operation отправляется пользователю io_service, пользовательский поток может быть заблокирован на неопределенный срок. Рассмотрим влияние на следующий код:

boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor(io_service); 
dir_monitor.add_directory(dir_name); 
// Post monitor_operation into io_service.
dir_monitor.async_monitor(...);
io_service.post(&user_handler);
io_service.run();

В приведенном выше коде, если io_service.run() сначала вызывает monitor_operation, тогда user_handler() не будет вызываться до тех пор, пока dir_monitor не увидит событие в каталоге dir_name. Следовательно, реализация службы dir_monitor не будет вести себя последовательно, что большинство пользователей ожидают от других служб.

Служба Asio Logger

Использование внутреннего потока и io_service:

  • Смягчает накладные расходы на ведение журнала в пользовательских потоках, выполняя потенциально блокирующие или дорогие вызовы во внутреннем потоке.
  • Гарантирует безопасность потока std::ofstream, так как только один внутренний поток записывает в поток. Если ведение журнала было выполнено непосредственно в logger_service::log() или если logger_service отправил свои обработчики в пользовательский io_service, для обеспечения безопасности потоков потребуется явная синхронизация. Другие механизмы синхронизации могут приводить к большей нагрузке или сложности в реализации.
  • Позволяет services регистрировать сообщения в shutdown_service(). В уничтожение io_service будет:

    • Завершение работы каждого из своих сервисов.
    • Уничтожьте всех неинвизированных обработчиков, которые были запланированы для отложенного вызова в io_service или любом из связанных с ним strand s.
    • Уничтожьте все свои службы.


    По мере того, как срок службы пользователя io_service закончился, его очередь событий не обрабатывается и не может быть отправлена ​​дополнительная обработка обработчиков. Имея собственный внутренний io_service, который обрабатывается собственным потоком, logger_service позволяет другим службам регистрировать сообщения во время их shutdown_service().


Дополнительные соображения

При реализации пользовательской службы рассмотрим несколько моментов:

  • Заблокировать все сигналы на внутренних потоках.
  • Никогда не вызывайте код пользователя напрямую.
  • Как отслеживать и отправлять обработчики пользователей при уничтожении реализации.
  • Ресурсы (ы), принадлежащие службе, которые разделены между реализациями служб.

В последних двух точках объект dir_monitor I/O демонстрирует поведение, которое пользователи не могут ожидать. Поскольку единственный поток внутри службы вызывает операцию блокировки в одной очереди событий реализации, он эффективно блокирует операции, которые потенциально могут быть немедленно завершены для их соответствующей реализации:

boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor1(io_service); 
dir_monitor1.add_directory(dir_name1); 
dir_monitor1.async_monitor(&handler_A);

boost::asio::dir_monitor dir_monitor2(io_service); 
dir_monitor2.add_directory(dir_name2); 
dir_monitor2.async_monitor(&handler_B);
// ... Add file to dir_name2.

{
  // Use scope to enforce lifetime.
  boost::asio::dir_monitor dir_monitor3(io_service); 
  dir_monitor3.add_directory(dir_name3); 
  dir_monitor3.async_monitor(&handler_C);
}
io_service.run();

Хотя операции, связанные с handler_B() (успех) и handler_C() (прерванные), не будут блокироваться, единственный поток в basic_dir_monitor_service заблокирован, ожидая изменения до dir_name1.