
Al tercer día, una nueva versión de SObjectizer estuvo disponible : 5.6.0 . Su característica principal es el rechazo de la compatibilidad con la rama estable anterior 5.5, que se ha desarrollado constantemente en el transcurso de cuatro años y medio.
Los principios básicos de funcionamiento de SObjectizer-5 seguían siendo los mismos. Comunicaciones, agentes, cooperaciones y despachadores todavía están con nosotros. Pero algo cambió seriamente, algo generalmente fue desechado. Por lo tanto, simplemente tomar SO-5.6.0 y volver a compilar su código fallará. Algo necesita ser reescrito. Es posible que haya que rediseñar algo.
¿Por qué nos ocupamos de la compatibilidad durante varios años y luego decidimos tomar y romper todo? ¿Y qué se rompió más a fondo?
Trataré de hablar sobre esto en este artículo.
¿Por qué necesitabas romper algo?
Es así de simple.
SObjectizer-5.5 durante su desarrollo ha absorbido tantas cosas diferentes y diversas que no fueron planificadas originalmente, que como resultado, ha formado demasiadas muletas y accesorios dentro. Con cada nueva versión, agregar algo nuevo a SO-5.5 se hizo cada vez más difícil. Y finalmente, a la pregunta "¿Por qué necesitamos todo esto?" No se encontró una respuesta adecuada.
Entonces, la primera razón es la nueva complicación de los menudillos de SObjectizer.
La segunda razón es que estamos estúpidamente cansados de centrarnos en los viejos compiladores de C ++. La rama 5.5 comenzó en 2014, cuando tuvimos, si no me equivoco, gcc-4.8 y MSVS2013. Y a este nivel, seguimos manteniendo la barra de requisitos para el nivel de soporte para el estándar C ++.
Inicialmente, teníamos "interés egoísta" en esto. Además, durante algún tiempo consideramos los bajos requisitos de calidad de soporte para el estándar C ++ como nuestra "ventaja competitiva".
Pero el tiempo continúa, el "interés egoísta" ha terminado. Algunos beneficios de tal "ventaja competitiva" no son visibles. Tal vez lo serían, si trabajáramos con C ++ 98, entonces estaríamos interesados en la empresa sangrienta. Pero la empresa sangrienta en aquellos como nosotros, en principio, no está interesada. Por lo tanto, se decidió dejar de limitarnos y tomar algo más fresco. Así que tomamos lo más fresco del establo en este momento: C ++ 17.
Obviamente, no a todos les gustará esta solución, después de todo, para muchos C ++ 17, esta es ahora una vanguardia inalcanzable, que todavía está muy, muy lejos.
Sin embargo, nos decidimos por tal riesgo. De todos modos, el proceso de popularización de SObjectizer no va rápido, por lo que cuando SObjectizer tenga una demanda más o menos amplia, C ++ 17 ya no será una "ventaja". Por el contrario, se tratará de la misma manera que ahora en C ++ 11.
En general, en lugar de continuar construyendo muletas usando un subconjunto de C ++ 11, decidimos rehacer seriamente las partes internas de SObjectizer usando C ++ 17. Para construir una base sobre la cual SObjectizer puede desarrollarse progresivamente durante los próximos cuatro o cinco años.
¿Qué ha cambiado seriamente en SObjectizer-5.6?
Ahora, repasemos brevemente algunos de los cambios más sorprendentes.
Las cooperaciones de agentes ya no tienen nombres de cadena
El problema
Desde el principio, SObjectizer-5 exigió que cada cooperación tenga su propio nombre de cadena único. Esta característica fue heredada por el quinto SObjectizer del cuarto SObjectizer anterior.
En consecuencia, SObjectizer necesitaba almacenar los nombres de las cooperaciones registradas. Verifique su singularidad en el registro. Busque cooperación por nombre durante la baja del registro, etc., etc.
Desde las primeras versiones, se ha utilizado un esquema simple en SObjectzer-5: un único diccionario de cooperaciones registradas protegidas por mutex. Al registrar una cooperación, se captura mutex, se verifica la unicidad del nombre de la cooperación, la presencia de un padre, etc. Después de verificar, el diccionario se modifica, después de lo cual se libera mutex. Esto significa que si al mismo tiempo comienza el registro / desregistro de varias cooperaciones a la vez, en algunos puntos se detendrán y esperarán hasta que una de las operaciones complete el trabajo con el diccionario cooperativo. Debido a esto, las operaciones cooperativas no escalaron bien.
De eso quería deshacerme para mejorar la situación con la velocidad de registro de las cooperaciones.
Solución
Se consideraron dos formas principales de resolver este problema.
En primer lugar, almacenar nombres de cadenas, pero cambiar la forma en que se almacena el diccionario para que la operación de registro de cooperación pueda escalar. Por ejemplo, el fragmento de diccionario, es decir rompiéndolo en varias piezas, cada una de las cuales estaría protegida por su mutex.
En segundo lugar, un rechazo completo de los nombres de cadena y el uso de algunos identificadores asignados por SObjectizer.
Como resultado, elegimos el segundo método y abandonamos por completo el nombramiento de las cooperativas. Ahora en SObjectizer hay algo como coop_handle
, es decir un identificador cuyo contenido está oculto para el usuario, pero que se puede comparar con std::weak_ptr
.
SObjectizer devuelve coop_handle
al registrar una colaboración:
auto coop = env.make_coop(); ...
Este identificador debe usarse para cancelar el registro de la cooperación:
auto coop = env.make_coop(); ...
Además, este identificador debe usarse al establecer una relación padre-hijo:
La estructura del repositorio para la cooperación dentro del entorno SObjectizer también ha cambiado drásticamente. Si antes de la versión 5.5 inclusive era un diccionario común, ahora cada cooperación es un depósito de enlaces a cooperaciones infantiles. Es decir Las cooperativas forman un árbol con una raíz en una cooperativa raíz especial oculta al usuario.
Dicha estructura permite escalar register_coop
deregister_coop
register_coop
y deregister_coop
mucho mejor: el bloqueo mutuo de operaciones paralelas se produce solo si ambas pertenecen a la misma cooperación principal. Para mayor claridad, aquí está el resultado del lanzamiento de un punto de referencia especial que mide el rendimiento de las operaciones con cooperaciones en mi computadora portátil anterior con Ubuntu 16.04 y GCC-7.3:
_test.bench.so_5.parallel_parent_child -r 4 -l 7 -s 5 Configuration: roots: 4, levels: 7, level-size: 5 parallel_parent_child: 15.69s 488280 488280 488280 488280 Total: 1953120
Es decir La versión 5.6.0 hizo frente a casi 2 millones de cooperaciones en ~ 15.5 segundos.
Y aquí está la versión 5.5.24.4, la última de la rama 5.5 en este momento:
_test.bench.so_5.parallel_parent_child -r 4 -l 7 -s 5 Configuration: roots: 4, levels: 7, level-size: 5 parallel_parent_child: 46.856s 488280 488280 488280 488280 Total: 1953120
El mismo escenario, pero el resultado es tres veces peor.
Solo queda un tipo de despachadores
Los despachadores son uno de los pilares de SObjectizer. Son los despachadores quienes determinan dónde y cómo los agentes procesarán sus mensajes. Entonces, sin la idea de despachadores, probablemente no habría habido un SObjectizer.
Sin embargo, los propios despachadores evolucionaron, evolucionaron y evolucionaron hasta el punto en que ni siquiera fue difícil para nosotros crear un nuevo despachador para SObjectizer-5.5. Pero muy problemático. Sin embargo, tomémoslo en orden.
Inicialmente, todos los despachadores que la aplicación necesitaba solo podían crearse al inicio del SObjectizer:
so_5::launch( []( so_5::environment_t & env ) { },
No creé el despachador necesario antes del inicio: todo, es mi culpa, no puedes cambiar nada.
Está claro que esto es inconveniente y, a medida que se ampliaron los escenarios de uso de SObjectizer, fue necesario resolver este problema. Por lo tanto, add_dispatcher_if_not_exists
método add_dispatcher_if_not_exists
, que verificó la presencia de un despachador y, si no había ninguno, permitió crear una nueva instancia:
so_5::launch( []( so_5::environment_t & env ) { ...
Dichos despachadores fueron llamados públicos. Los despachadores públicos tenían nombres únicos. Y usando estos nombres, los agentes estaban obligados a los despachadores:
so_5::launch( []( so_5::environment_t & env ) { ...
Pero los despachadores públicos tenían una característica desagradable. Comenzaron a trabajar inmediatamente después de agregarlos al entorno SObjectizer y continuaron trabajando hasta que el entorno SObjectizer completó su trabajo.
De nuevo, con el tiempo, comenzó a interferir. Era necesario asegurarse de que los despachadores pudieran agregarse según fuera necesario y que los despachadores que se volvieran innecesarios se eliminaran automáticamente.
Así que había despachadores "privados". Estos despachadores no tenían nombres y vivían mientras hubiera referencias a ellos. Se podían crear despachadores privados en cualquier momento después de iniciar el entorno SObjectizer, se destruían automáticamente.
En general, los despachadores privados resultaron ser un enlace muy exitoso en la evolución de los despachadores, pero trabajar con ellos fue muy diferente de trabajar con despachadores públicos:
so_5::launch( []( so_5::environment_t & env ) { ...
Incluso más despachadores privados y públicos diferían en la implementación. Por lo tanto, para no duplicar el código y escribir por separado el despachador público y privado del mismo tipo, tuve que usar construcciones bastante complejas con plantillas y herencia.
Como resultado, toda esta variedad estaba cansada de acompañar, y en SObjectizer-5.6 solo quedaba un tipo de despachadores. De hecho, este es un análogo de despachadores privados. Pero solo sin una mención explícita de la palabra "privado". Entonces, el fragmento que se muestra arriba se escribirá como:
so_5::launch( []( so_5::environment_t & env ) { ...
Solo quedan funciones gratuitas send, send_delayed y send_periodic left
El desarrollo de la API para enviar mensajes a SObjectizer es probablemente el ejemplo más sorprendente de cómo SObjectizer ha cambiado a medida que el soporte para C ++ 11 ha mejorado en los compiladores disponibles para nosotros.
Primero, los mensajes se enviaron así:
mbox->deliver_message(new my_message(...));
O, si sigue las "recomendaciones de los mejores criadores de perros" (c):
std::unique_ptr<my_message> msg(new my_message(...)); mbox->deliver_message(std::move(msg));
Sin embargo, luego tuvimos a nuestra disposición compiladores con soporte para plantillas variadas y aparecieron funciones de envío. Se hizo posible escribir así:
send<my_message>(target, ...);
Es cierto que a toda una familia le tomó más tiempo send_to_agent
partir de simples send
, incluidos send_to_agent
, send_delayed_to_agent
, etc. Y luego, para hacer que esta familia se reduzca al conjunto familiar de send
, send_delayed
y send_periodic
.
Pero, a pesar del hecho de que la familia de funciones de envío se formó hace bastante tiempo y ha sido la forma recomendada de enviar mensajes durante varios años, los métodos antiguos como deliver_message
, schedule_timer
y single_timer
todavía estaban disponibles para el usuario.
Pero en la versión 5.6.0, solo las funciones gratuitas send
, send_delayed
y send_periodic
se guardaron en la API pública de SObjectizer. Todo lo demás se eliminó por completo o se transfirió a espacios de nombres internos de SObjectizer.
Entonces, en SObjectizer-5.6, la interfaz para enviar mensajes finalmente se ha convertido en lo que hubiera sido si hubiéramos tenido compiladores con soporte C ++ 11 normal desde el principio. Bueno, además de eso, si tuviéramos experiencia usando este C ++ 11 muy normal.
Con las send_periodic
send_delayed
y send_periodic
en versiones anteriores de SObjectizer, hubo otro incidente.
Para usar el temporizador, debe tener acceso al entorno SObjectizer. Dentro del agente hay un enlace al entorno SObjectizer. Y dentro de mchain hay tal enlace. Pero dentro del mbox no estaba allí. Por lo tanto, si se envió un mensaje pendiente a un agente o a mchain, la llamada send_delayed
así:
send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...);
Para el caso de mbox, tuvimos que tomar un enlace al SObjectizer Environment desde otro lugar:
send_delayed<my_message>(this->so_environment(), target_mbox, pause, ...);
Esta característica de send_delayed
y send_periodic
fue una pequeña astilla. Lo cual no interfiere mucho, pero es bastante molesto. Y todo porque inicialmente no comenzamos a almacenar el enlace a SObjectizer Environment en mbox-ahs.
La violación de la compatibilidad con versiones anteriores fue una buena razón para deshacerse de esta astilla.
Ahora puede averiguar desde mbox para qué SObjectizer Environment fue creado. Y esto hizo posible utilizar los send_delayed
únicos send_delayed
y send_periodic
para cualquier tipo de destinatario de mensaje de temporizador:
send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...); send_delayed<my_message>(target_mbox, pause, ...);
En el sentido literal, "un poco, pero agradable".
No más agentes ad-hoc.
Como dice el refrán, "Cada accidente tiene un nombre, segundo nombre y apellido". En el caso de agentes ad-hoc, este es mi primer nombre, segundo nombre y apellido :(
El punto es este. Cuando comenzamos a hablar sobre SObjectizer-5 en público, escuchamos muchos reproches por la verbosidad del código de los ejemplos de SObjectizer. Y personalmente, esta verbosidad me pareció un problema grave con el que tengo que tratar seriamente.
Una fuente de verbosidad es la necesidad de que los agentes hereden del tipo base especial agent_t
. Y de esto, parecería, no hay escapatoria. O no?
Entonces había agentes ad-hoc, es decir agentes, para la determinación de que no era necesario escribir una clase separada, solo fue suficiente para establecer la reacción a los mensajes en forma de funciones lambda. Por ejemplo, el ejemplo clásico de ping-pong en agentes ad-hoc podría escribirse así:
auto pinger = coop->define_agent(); auto ponger = coop->define_agent(); pinger .on_start( [ponger]{ so_5::send< msg_ping >( ponger ); } ) .event< msg_pong >( pinger, [ponger]{ so_5::send< msg_ping >( ponger ); } ); ponger .event< msg_ping >( ponger, [pinger]{ so_5::send< msg_pong >( pinger ); } );
Es decir No hay clases propias. Solo llamamos a define_agent()
en la cooperación y obtenemos algún tipo de objeto de agente, que puede suscribirse a los mensajes entrantes.
Entonces, en SObjectizer-5 hubo una separación en agentes regulares y ad-hoc.
Lo que no trajo ninguna bonificación visible, solo los costos laborales adicionales para acompañar tal separación. Y con el tiempo, se hizo evidente que los agentes ad-hoc son como una maleta sin asa: es difícil de transportar y es una pena irse. Pero mientras trabajaba en SObjectizer-5.6, se decidió abandonar.
Al mismo tiempo, también se aprendió otra lección, quizás aún más importante: en cualquier discusión pública de la herramienta en Internet, participará una gran cantidad de personas que son indiferentes a lo que es la herramienta, por qué se necesita, por qué se supone que se debe usar, etc. Es simplemente importante para ellos expresar su fuerte opinión. En el segmento de Internet en ruso, además de esto, sigue siendo muy importante transmitir a los desarrolladores de la herramienta cuán locos y sin educación son y cuánto no se necesita el resultado de su trabajo.
Por lo tanto, debe tener mucho cuidado con lo que le dicen. Y puede escuchar (y luego con cuidado) solo lo que se dice aquí en este sentido: "Traté de hacer esto en su instrumento y no me gusta la cantidad de código que tiene aquí". Incluso tales deseos deben tratarse con mucho cuidado: "Tomaría tu desarrollo si fuera más fácil aquí y aquí".
Desafortunadamente, la habilidad de "filtrar" que dijeron los "simpatizantes" en Internet hace unos cinco años era mucho menor que ahora. Por lo tanto, un experimento tan específico como agentes ad-hoc en SObjectizer.
SObjectizer-5.6 ya no admite la interacción de agente sincronizada
El tema de la interacción sincronizada entre agentes es muy antiguo y doloroso.
Comenzó en los días de SObjectizer-4. Y en SObjectizer-5 continuó. Hasta ahora, finalmente, el llamado solicitudes de servicio Lo que inicialmente, sin duda, daba miedo como la muerte. Pero luego logré darles un aspecto más o menos decente .
Pero este resultó ser el caso cuando el primer panqueque salió grumoso :(
Dentro de SObjectizer, tuve que implementar la entrega y el procesamiento de mensajes regulares de una manera, y la entrega y el procesamiento de solicitudes sincrónicas en otra. Es especialmente triste que estas características tuvieran que tenerse en cuenta, incluso al implementar sus propios mbox-s.
Y después de que se agregó la funcionalidad de los mensajes de sobre a SObjectizer, se hizo necesario observar aún más a menudo y más a fondo las diferencias entre los mensajes regulares y las solicitudes sincrónicas.
En general, con solicitudes síncronas durante el mantenimiento / desarrollo de SObjectizer, hubo demasiado dolor de cabeza. Tanto es así que al principio hubo un deseo concreto de deshacerse de estas solicitudes muy sincronizadas . Y entonces este deseo se hizo realidad.
Y así, en SObjectizer-5.6, los agentes pueden interactuar nuevamente solo a través de mensajes asincrónicos.
Y dado que a veces todavía se necesita algo como la interacción sincrónica, se ha enviado soporte para este tipo de interacción al proyecto so5extra que lo acompaña :
Es decir Ahora trabajar con solicitudes síncronas es fundamentalmente diferente en que el controlador de solicitudes no devuelve un valor del método de controlador, como lo era antes. En su lugar, se make_reply
método make_reply
.
La nueva implementación es buena ya que tanto la solicitud como la respuesta se envían dentro del SObjectizer como mensajes asíncronos regulares. De hecho, make_reply
es una implementación un poco más específica de send
.
Y, lo que es más importante, la nueva implementación nos permitió obtener una funcionalidad que antes era inalcanzable:
- Las solicitudes síncronas (es decir,
request_reply_t<Request, Reply>
objetos) ahora se pueden guardar y / o reenviar a otros controladores. Lo que hace posible implementar varios esquemas de equilibrio de carga; - Puede hacer que la respuesta a la solicitud venga en un mbox regular del agente que inicia la solicitud. Y el agente iniciador procesará la respuesta de la manera habitual, como cualquier otro mensaje;
- Puede enviar varias solicitudes a diferentes destinatarios a la vez y luego analizar sus respuestas en el orden en que fueron recibidas:
using first_dialog = so_5::extra::sync::request_reply_t<first_request, first_reply>; using second_dialog = so_5::extra::sync::request_reply_t<second_request, second_reply>;
Entonces, podemos decir que con la interacción sincrónica en SObjectizer sucedió lo siguiente:
- durante mucho tiempo se fue por razones ideológicas;
- luego se agregó y resultó que a veces esa interacción es útil;
- pero la experiencia ha demostrado que la primera implementación no es muy exitosa;
- la implementación anterior se descartó por completo y, a cambio, se propuso una implementación nueva.
Trabajaron en sus propios errores, en general.
Conclusión
Este artículo, brevemente, habló sobre varios cambios en SObjectizer-5.6.0 y las razones detrás de estos cambios.
Puede encontrar una lista más completa de cambios aquí .
En conclusión, me gustaría ofrecer a aquellos que aún no han probado SObjectizer, tómelo y pruébelo. Y comparte con nosotros tus sentimientos: lo que te gustó, lo que no te gustó, lo que faltaba.
Escuchamos atentamente todos los comentarios constructivos / sugerencias. Además, en los últimos años, solo lo que alguien necesita está incluido en SObjectizer. Entonces, si no nos dice qué le gustaría tener en SObjectizer, esto no aparecerá. Y si me dices, entonces quién sabe ...;)
El proyecto ahora vive y se desarrolla aquí . Y para aquellos que están acostumbrados a usar solo GitHub, hay un espejo GitHub . Este espejo es completamente nuevo, por lo que puede ignorar la falta de estrellas.
PS. Puede seguir las noticias relacionadas con SObjectizer en este grupo de Google . Allí puede plantear problemas relacionados con SObjectizer.