c++ - Intentando comprender la implementación del servicio personalizado de Boost.Asio
boost-asio (1)
Estoy pensando en escribir un servicio personalizado de Asio además de un protocolo de red de terceros propio que actualmente estamos usando.
Según la guía Highscore Asio , debe implementar tres clases para crear un servicio personalizado de Asio:
- Una clase derivada de
boost::asio::basic_io_object
representa el nuevo objeto de E / S. - Una clase derivada de
boost::asio::io_service::service
representa un servicio que está registrado con el servicio de E / S y se puede acceder desde el objeto de E / S. - Una clase no derivada de ninguna otra clase que represente la implementación del servicio.
La implementación del protocolo de red ya proporciona operaciones asincrónicas y tiene un bucle de evento (de bloqueo). Así que pensé, lo pondría en mi clase de implementación de servicios y ejecutaría el bucle de eventos en un hilo de trabajo interno. Hasta aquí todo bien.
Al observar algunos ejemplos de servicios personalizados, noté que las clases de servicio engendran sus propios hilos internos (de hecho crean instancias de sus propias instancias internas de io_service). Por ejemplo:
La página Highscore proporciona un ejemplo de monitor de directorio . Es esencialmente una envoltura alrededor de inotify. Las clases interesantes son
inotify/basic_dir_monitor_service.hpp
einotify/dir_monitor_impl.hpp
.Dir_monitor_impl
maneja la interacción real con inofity, que es de bloqueo, y por lo tanto se ejecuta en una cadena de fondo. Estoy de acuerdo con eso. Pero elbasic_dir_monitor_service
también tiene un subproceso interno de trabajo y todo lo que parece estar haciendo es mezclar solicitudes entre main''sio_service
ydir_monitor_impl
.basic_dir_monitor_service
con el código,basic_dir_monitor_service
hilo de trabajo enbasic_dir_monitor_service
y, en su lugar,basic_dir_monitor_service
solicitudes directamente al servicio io_ principal y el programa aún se ejecutó como antes.En el ejemplo del servicio de registro personalizado de Asio, he notado el mismo enfoque. El servicio
logger_service
genera un hilo de trabajo interno para manejar las solicitudes de registro. No he tenido tiempo de jugar con ese código, pero creo que también debería ser posible publicar estas solicitudes directamente en el servicio principal de io.
¿Cuál es la ventaja de tener estos "trabajadores intermediarios"? ¿No podría publicar todo el trabajo en el io_service principal todo el tiempo? ¿Extrañé algún aspecto crucial del patrón de Proactor?
Probablemente debería mencionar que estoy escribiendo software para un sistema incrustado de un solo núcleo. Tener estos hilos adicionales en su lugar parece imponer cambios de contexto innecesarios que me gustaría evitar si es posible.
En resumen, consistencia. Los servicios intentan cumplir con las expectativas del usuario establecidas por los servicios de Boost.Asio.
El uso de un io_service
interno proporciona una clara separación de la propiedad y el control de los controladores. Si un servicio personalizado publica sus controladores internos en el io_service
del usuario, la ejecución de los controladores internos del servicio queda acoplada implícitamente con los controladores del usuario. Considere cómo esto afectaría las expectativas del usuario con el ejemplo del Boost.Asio Logger Service :
- El servicio
logger_service
escribe en la secuencia de archivos dentro de un controlador. Por lo tanto, un programa que nunca procesa el bucle de eventosio_service
, como uno que solo usa la API síncrona, nunca tendrá mensajes de registro escritos. - El servicio
logger_service
ya no sería seguro para subprocesos, lo que podríaio_service
un comportamiento indefinido si varios subprocesos procesan el servicioio_service
. - La duración de las operaciones internas de
logger_service
está restringida por la del servicioio_service
. Por ejemplo, cuando se invoca la funciónshutdown_service()
un servicio, la duración del servicioio_service
ya ha finalizado. Por lo tanto, los mensajes no se pudieron registrar a través delogger_service::log()
dentro deshutdown_service()
, ya que intentaría publicar un controlador interno en elio_service
cuya vida útil ya ha finalizado. El usuario ya no puede asumir un mapeo uno a uno entre una operación y el manejador. Por ejemplo:
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.
En este caso,
io_service.poll()
puede invocar el controlador interno allogger_service
, en lugar deconnect_handler()
.
Además, estos subprocesos internos intentan imitar el comportamiento utilizado internamente por Boost.Asio itself :
La implementación de esta biblioteca para una plataforma particular puede hacer uso de uno o más subprocesos internos para emular la asincronicidad. En la medida de lo posible, estos hilos deben ser invisibles para el usuario de la biblioteca.
Ejemplo de Directory Monitor
En el ejemplo de monitor de directorio, se utiliza un subproceso interno para evitar el bloqueo indefinido del io_service
del usuario mientras se espera un evento. Una vez que se ha producido un evento, el controlador de finalización está listo para ser invocado, por lo que el subproceso interno publica el controlador de usuario en el io_service
del usuario para la invocación diferida. Esta implementación emula asynchronicity con un hilo interno que es en su mayoría invisible para el usuario.
Para obtener más información, cuando se inicia una operación de monitor asíncrona a través de dir_monitor::async_monitor()
, se basic_dir_monitor_service::monitor_operation
un basic_dir_monitor_service::monitor_operation
en el io_service
interno. Cuando se invoca, esta operación invoca dir_monitor_impl::popfront_event()
, una llamada potencialmente bloqueante. Por lo tanto, si el monitor_operation
se publica en el io_service
del usuario, el hilo del usuario podría bloquearse indefinidamente. Considere el efecto en el siguiente código:
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();
En el código anterior, si io_service.run()
invoca monitor_operation
primero, entonces user_handler()
no se invocará hasta que dir_monitor
observe un evento en el directorio dir_name
. Por lo tanto, la implementación del servicio dir_monitor
no se comportaría de manera consistente que la mayoría de los usuarios esperan de otros servicios.
Servicio Asio Logger
El uso de un hilo interno y io_service
:
- Mitiga la sobrecarga de iniciar sesión en el (los) hilo (s) del usuario realizando llamadas potencialmente costosas o de bloqueo dentro del hilo interno.
- Garantiza la seguridad de subprocesos de
std::ofstream
, ya que solo el único subproceso interno escribe en la secuencia. Si el registro se realizó directamente dentro delogger_service::log()
o silogger_service
publicó sus manejadores en elio_service
del usuario, entonces se requeriría una sincronización explícita para la seguridad de los hilos. Otros mecanismos de sincronización pueden introducir una mayor sobrecarga o complejidad en la implementación. Permite que los
services
registren mensajes dentro deshutdown_service()
. Durante la destruction , elio_service
:- Cierre cada uno de sus servicios.
- Destruye todos los controladores no invocados que estaban programados para la invocación diferida en el
io_service
o cualquiera de susstrand
asociadas. - Destruye cada uno de sus servicios.
Como la vida útil del servicioio_service
del usuario ha finalizado, su cola de eventos no se está procesando ni se pueden publicar controladores adicionales. Al tener su propioio_service
interno procesado por su propio hilo,logger_service
permite a otros servicios registrar mensajes duranteshutdown_service()
.
consideraciones adicionales
Al implementar un servicio personalizado, aquí hay algunos puntos a considerar:
- Bloquea todas las señales en los hilos internos.
- Nunca invoque el código del usuario directamente.
- Cómo rastrear y publicar controladores de usuario cuando se destruye una implementación.
- Recurso (s) propiedad del servicio que se comparten entre las implementaciones del servicio.
Para los dos últimos puntos, el dir_monitor
E / S dir_monitor
exhibe un comportamiento que los usuarios pueden no esperar. Como el único hilo dentro del servicio invoca una operación de bloqueo en la cola de eventos de una sola implementación, bloquea efectivamente las operaciones que podrían completarse de manera inmediata para su respectiva implementación:
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();
Aunque las operaciones asociadas con handler_B()
(success) y handler_C()
(abortado) no se bloquearían, el hilo único en basic_dir_monitor_service
está bloqueado esperando un cambio a dir_name1
.