En mi nuevo equipo que administro, la mayoría de nuestro código es plataforma, socket TCP y código de red http. Todos los C ++. La mayor parte se originó de otros desarrolladores que han abandonado el equipo. Los desarrolladores actuales en el equipo son muy inteligentes, pero en su mayoría junior en términos de experiencia.
Nuestro mayor problema: errores de concurrencia multiproceso. La mayoría de nuestras bibliotecas de clases están escritas para ser asíncronas mediante el uso de algunas clases de grupo de subprocesos. Los métodos en las bibliotecas de clases a menudo ponen en cola tareas largas en el grupo de subprocesos desde un subproceso y luego los métodos de devolución de llamada de esa clase se invocan en un subproceso diferente. Como resultado, tenemos muchos errores de casos extremos que implican suposiciones de subprocesos incorrectas. Esto da como resultado errores sutiles que van más allá de solo tener secciones y bloqueos críticos para proteger contra problemas de concurrencia.
Lo que hace que estos problemas sean aún más difíciles es que los intentos de solucionarlos a menudo son incorrectos. Algunos errores que he observado que el equipo intenta (o dentro del código heredado) incluye algo como lo siguiente:
Error común n. ° 1 : solucionar el problema de concurrencia simplemente bloqueando los datos compartidos, pero olvidando lo que sucede cuando los métodos no se llaman en el orden esperado. Aquí hay un ejemplo muy simple:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Entonces, ahora tenemos un error en el que se puede llamar a Shutdown mientras OnHttpNetworkRequestComplete está ocurriendo. Un probador encuentra el error, captura el volcado de memoria y asigna el error a un desarrollador. Él a su vez corrige el error así.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
La solución anterior se ve bien hasta que te das cuenta de que hay un caso de borde aún más sutil. ¿Qué sucede si se llama a Shutdown antes de que se vuelva a llamar a OnHttpRequestComplete? Los ejemplos del mundo real que tiene mi equipo son aún más complejos, y los casos extremos son aún más difíciles de detectar durante el proceso de revisión del código.
Error común n. ° 2 : solucionar problemas de punto muerto al salir ciegamente de la cerradura, esperar a que termine el otro subproceso, luego volver a ingresar al candado, ¡pero sin manejar el caso de que el otro subproceso acaba de actualizar el objeto!
Error común n. ° 3 : aunque los objetos se cuentan por referencia, la secuencia de apagado "libera" su puntero. Pero se olvida de esperar el hilo que aún se está ejecutando para liberar su instancia. Como tal, los componentes se apagan limpiamente, luego se invocan devoluciones de llamada espurias o tardías en un objeto en un estado que no espera más llamadas.
Hay otros casos extremos, pero la conclusión es esta:
La programación multiproceso es sencillamente difícil, incluso para personas inteligentes.
Al detectar estos errores, paso tiempo discutiendo los errores con cada desarrollador para desarrollar una solución más adecuada. Pero sospecho que a menudo están confundidos sobre cómo resolver cada problema debido a la enorme cantidad de código heredado que la solución "correcta" implicará tocar.
Pronto enviaremos, y estoy seguro de que los parches que estamos aplicando se mantendrán para el próximo lanzamiento. Después, tendremos algo de tiempo para mejorar la base del código y refactorizar donde sea necesario. No tendremos tiempo para volver a escribir todo. Y la mayoría del código no es tan malo. Pero estoy buscando refactorizar el código de modo que los problemas de subprocesos se puedan evitar por completo.
Un enfoque que estoy considerando es este. Para cada característica importante de la plataforma, tenga un hilo único dedicado donde todos los eventos y devoluciones de llamadas de la red se agrupen. Similar al subproceso de apartamentos COM en Windows con el uso de un bucle de mensajes. Las operaciones de bloqueo largas aún podrían enviarse a un subproceso de grupo de trabajo, pero la devolución de llamada de finalización se invoca en el subproceso del componente. Los componentes podrían incluso compartir el mismo hilo. Entonces, todas las bibliotecas de clases que se ejecutan dentro del subproceso se pueden escribir bajo la suposición de un mundo único con subprocesos.
Antes de seguir ese camino, también estoy muy interesado si existen otras técnicas estándar o patrones de diseño para tratar problemas de subprocesos múltiples. Y tengo que enfatizar, algo más allá de un libro que describe los conceptos básicos de mutexes y semáforos. ¿Qué piensas?
También estoy interesado en cualquier otro enfoque para adoptar un proceso de refactorización. Incluyendo cualquiera de los siguientes:
Literatura o documentos sobre patrones de diseño en torno a hilos. Algo más allá de una introducción a mutexes y semáforos. Tampoco necesitamos paralelismo masivo, solo formas de diseñar un modelo de objeto para manejar eventos asincrónicos de otros hilos correctamente .
Formas de diagramar el enhebrado de varios componentes, para que sea fácil estudiar y desarrollar soluciones. (Es decir, un equivalente UML para discutir hilos a través de objetos y clases)
Educar a su equipo de desarrollo sobre los problemas con el código multiproceso.
¿Qué harías?
fuente
Respuestas:
Su código tiene otros problemas importantes además de eso. ¿Eliminar manualmente un puntero? ¿Llamando a una
cleanup
función? Búho Además, como se señala con precisión en el comentario de la pregunta, no utiliza RAII para su bloqueo, que es otro error bastante épico y garantiza que cuando seDoSomethingImportant
lanza una excepción, suceden cosas terribles.El hecho de que este error multiproceso esté ocurriendo es solo un síntoma del problema central: su código tiene una semántica extremadamente mala en cualquier situación de subprocesamiento y está utilizando herramientas y expresiones idiomáticas completamente poco confiables. Si yo fuera usted, me sorprendería que funcione con un solo hilo, y mucho menos más.
Todo el punto de conteo de referencias es que el hilo ya ha lanzado su instancia . Porque si no, entonces no se puede destruir porque el hilo todavía tiene una referencia.
Uso
std::shared_ptr
. Cuando todas las discusiones han dado a conocer (y nadie , por lo tanto, puede ser llamada a la función, ya que no tienen puntero a ella), a continuación, se llama al destructor. Esto está garantizado seguro.En segundo lugar, use una biblioteca de subprocesos reales, como los bloques de creación de subprocesos de Intel o la biblioteca de patrones paralelos de Microsoft. Escribir el suyo es lento y poco confiable y su código está lleno de detalles de subprocesos que no necesita. Hacer tus propias cerraduras es tan malo como hacer tu propia administración de memoria. Ya han implementado muchos modismos de subprocesos muy útiles de uso general que funcionan correctamente para su uso.
fuente
Otros carteles han comentado bien lo que debe hacerse para solucionar los problemas centrales. Esta publicación se refiere al problema más inmediato de parchear el código heredado lo suficientemente bien como para ganar tiempo para rehacer todo de la manera correcta. En otras palabras, esta no es la forma correcta de hacer las cosas, es solo una forma de cojear por ahora.
Su idea de consolidar eventos clave es un buen comienzo. Llegaría al extremo de usar un solo hilo de despacho para manejar todos los eventos de sincronización de claves, donde sea que haya dependencia de orden. Configure una cola de mensajes segura para subprocesos y donde sea que realice actualmente operaciones sensibles a la concurrencia (asignaciones, limpiezas, devoluciones de llamada, etc.), envíe un mensaje a ese hilo y haga que realice o active la operación. La idea es que este hilo controle todos los inicios, paradas, asignaciones y limpiezas de la unidad de trabajo.
El hilo de despacho no resuelve los problemas que describió, solo los consolida en un solo lugar. Aún debe preocuparse por los eventos / mensajes que ocurren en un orden inesperado. Los eventos con tiempos de ejecución significativos aún deberán enviarse a otros subprocesos, por lo que todavía hay problemas con la concurrencia en los datos compartidos. Una forma de mitigar eso es evitar pasar datos por referencia. Siempre que sea posible, los datos en los mensajes de envío deben ser copias que serán propiedad del destinatario. (Esto está en la línea de hacer que los datos sean inmutables que otros han mencionado).
La ventaja de este enfoque de despacho es que dentro del hilo de despacho tiene un tipo de refugio seguro donde al menos sabe que ciertas operaciones están ocurriendo secuencialmente. La desventaja es que crea un cuello de botella y una sobrecarga adicional de la CPU. Sugiero que no se preocupe por ninguna de esas cosas al principio: concéntrese primero en obtener cierta medida de funcionamiento correcto moviéndose lo más que pueda al hilo de despacho. Luego, realice un perfil para ver qué ocupa la mayor parte del tiempo de CPU y comience a cambiarlo fuera del hilo de envío utilizando las técnicas de subprocesamiento múltiple correctas.
Una vez más, lo que estoy describiendo no es la forma correcta de hacer las cosas, pero es un proceso que puede llevarlo hacia la forma correcta en incrementos lo suficientemente pequeños como para cumplir con los plazos comerciales.
fuente
Según el código que se muestra, tiene un montón de WTF. Es extremadamente difícil, si no imposible, corregir gradualmente una aplicación multiproceso mal escrita. Dígales a los propietarios que la aplicación nunca será confiable sin una revisión significativa. Déles una estimación basada en la inspección y reelaboración de cada parte del código que interactúa con los objetos compartidos. Primero deles una estimación para la inspección. Luego puede dar un estimado para el retrabajo.
Cuando vuelva a trabajar el código, debe planear escribir el código para que sea probablemente correcto. Si no sabes cómo hacerlo, busca a alguien que lo haga o terminarás en el mismo lugar.
fuente
Si tiene tiempo para dedicar a refactorizar su aplicación, le aconsejaría que eche un vistazo al modelo de actor (consulte, por ejemplo , Theron , Casablanca , libcppa , CAF para implementaciones de C ++).
Los actores son objetos que se ejecutan simultáneamente y se comunican entre sí solo mediante el intercambio de mensajes asíncrono. Entonces, todos los problemas de gestión de subprocesos, mutexes, puntos muertos, etc., son tratados por una biblioteca de implementación de actores y puede concentrarse en implementar el comportamiento de sus objetos (actores), que se reduce a repetir el ciclo
Un enfoque para usted podría ser leer un poco sobre el tema primero, y posiblemente echar un vistazo a una o dos bibliotecas para ver si el modelo de actor puede integrarse en su código.
He estado utilizando (una versión simplificada de) este modelo en un proyecto mío durante algunos meses y estoy sorprendido de lo robusto que es.
fuente
El error aquí no es el "olvidar", sino el "no arreglarlo". Si tiene cosas que suceden en un orden inesperado, tiene un problema. Debe resolverlo en lugar de tratar de evitarlo (golpear un bloqueo en algo suele ser una solución alternativa).
Debe intentar adaptar el modelo / mensaje del actor hasta cierto punto y tener una separación de preocupación. El papel de
Foo
es claramente manejar algún tipo de comunicación HTTP. Si desea diseñar su sistema para hacer esto en paralelo, es la capa superior la que debe manejar los ciclos de vida de los objetos y acceder a la sincronización en consecuencia.Intentar que varios hilos operen con los mismos datos mutables es difícil. Pero también rara vez es necesario. Todos los casos comunes que exigen esto ya se han resumido en conceptos más manejables y se han implementado varias veces para cualquier lenguaje imperativo importante. Solo tienes que usarlos.
fuente
Sus problemas son bastante graves, pero típicos del mal uso de C ++. La revisión de código solucionará algunos de estos problemas. 30 minutos, un conjunto de globos oculares produce el 90% de los resultados (cita para esto es googleable)
Problema n. ° 1 Debe asegurarse de que haya una estricta jerarquía de bloqueo para evitar el bloqueo de bloqueo.
Si reemplaza Autolock con un contenedor y una macro, puede hacer esto.
Mantenga un mapa global estático de bloqueos creados en la parte posterior de su contenedor. Utiliza una macro para insertar la información del nombre del finen y del número de línea en el constructor del contenedor Autolock.
También necesitarás un gráfico dominador estático.
Ahora dentro del candado, debe actualizar el gráfico dominador, y si obtiene un cambio de orden, afirma un error y aborta.
Después de extensas pruebas, puede deshacerse de la mayoría de los puntos muertos latentes.
El código se deja como ejercicio para el alumno.
El problema n. ° 2 desaparecerá (principalmente)
Su solución de archivo va a funcionar. Lo he usado antes en misiones y sistemas de vida. Mi opinión sobre esto es esto
No comparta datos a través de variables públicas o captadores.
Los eventos externos llegan a través de un despacho multiproceso a una cola atendida por un subproceso. Ahora puede razonar sobre el manejo de eventos.
Los cambios de datos que cruzan subprocesos entran en un qeuue seguro para subprocesos, se manejan con un subproceso. Haz suscripciones. Ahora puede razonar sobre los flujos de datos.
Si sus datos necesitan ir al otro lado de la ciudad, publíquelos en la cola de datos. Eso lo copiará y lo pasará a los suscriptores de forma asincrónica. También rompe todas las dependencias de datos en el programa.
Esto es más o menos un modelo de actor barato. Los enlaces de Giorgio ayudarán.
Finalmente, su problema con los objetos cerrados.
Cuando estás contando referencias, has resuelto el 50%. El otro 50% es para referencias de devoluciones de llamadas. Pase los titulares de devolución de llamada una referencia. La llamada de apagado tiene que esperar el recuento cero en el recuento. No resuelve gráficos complicados de objetos; eso es entrar en la recolección de basura real. (Cuál es la motivación en Java para no hacer ninguna promesa sobre cuándo o si se llamará a finalize (); para que salgas de la programación de esa manera).
fuente
Para futuros exploradores: para complementar la respuesta sobre el modelo de actor, me gustaría agregar CSP ( procesos secuenciales de comunicación ), con un guiño a la familia más grande de cálculos de procesos en los que se encuentra. CSP es similar al modelo de actor, pero se divide de manera diferente. Todavía tiene un montón de subprocesos, pero se comunican a través de canales específicos, en lugar de específicamente entre sí, y ambos procesos deben estar listos para enviar y recibir respectivamente antes de que suceda. También hay un lenguaje formal para probar que el código CSP es correcto. Todavía estoy haciendo la transición para usar CSP en gran medida, pero lo he estado usando en algunos proyectos durante algunos meses, ahora, y es algo muy simplificado.
La Universidad de Kent tiene una implementación de C ++ ( https://www.cs.kent.ac.uk/projects/ofa/c++csp/ , clonada en https://github.com/themasterchef/cppcsp2 ).
fuente
Actualmente estoy leyendo esto y explica todos los problemas que puede obtener y cómo evitarlos, en C ++ (usando la nueva biblioteca de subprocesos, pero creo que las explicaciones globales son válidas para su caso): http: //www.amazon. com / C-Concurrency-Action-Practical-Multithreading / dp / 1933988770 / ref = sr_1_1? ie = UTF8 & qid = 1337934534 & sr = 8-1
Personalmente uso un UML simplificado y asumo que los mensajes se realizan de forma asincrónica. Además, esto es cierto entre "módulos", pero dentro de los módulos no quiero tener que saber.
El libro ayudaría, pero creo que los ejercicios / prototipos y el mentor experimentado serían mejores.
Evitaría totalmente que las personas que no entiendan los problemas de concurrencia trabajen en el proyecto. Pero supongo que no puedes hacer eso, así que en tu caso específico, aparte de tratar de asegurarte de que el equipo esté más educado, no tengo idea.
fuente
Ya está en camino al reconocer el problema y buscar activamente una solución. Esto es lo que haría:
fuente
Mirando su ejemplo: Tan pronto como Foo :: Shutdown comience a ejecutarse, no debe ser posible llamar a OnHttpRequestComplete para que se ejecute más. Eso no tiene nada que ver con ninguna implementación, simplemente no puede funcionar.
También podría argumentar que Foo :: Shutdown no debería ser invocable mientras se ejecuta una llamada a OnHttpRequestComplete (definitivamente cierto) y probablemente no si una llamada a OnHttpRequestComplete aún está pendiente.
Lo primero que hay que hacer bien es no bloquear, etc., sino la lógica de lo que está permitido o no. Un modelo simple sería que su clase puede tener cero o más solicitudes incompletas, cero o más finalizaciones que aún no se han llamado, cero o más finalizaciones que se están ejecutando, y que su objeto desea cerrarse o no.
Se esperaría que Foo :: Shutdown terminara de completar las ejecuciones, ejecute solicitudes incompletas hasta el punto en que puedan cerrarse si es posible, para no permitir que se inicien más finalizaciones, para no permitir que se inicien más solicitudes.
Lo que debe hacer: agregue especificaciones a sus funciones que indiquen exactamente lo que harán. (Por ejemplo, iniciar una solicitud http puede fallar después de que se haya llamado al apagado). Y luego escriba sus funciones para que cumplan con las especificaciones.
Los bloqueos se utilizan mejor solo durante el menor tiempo posible para controlar la modificación de variables compartidas. Por lo tanto, puede tener una variable "performShutDown" que está protegida por un bloqueo.
fuente
Sinceramente; Me había escapado rápidamente.
Los problemas de concurrencia son desagradables . Algo puede funcionar perfectamente durante meses y luego (debido al momento específico de varias cosas) explota repentinamente en la cara del cliente, sin forma de averiguar qué sucedió, sin esperanza de ver un informe de error agradable (reproducible) y de ninguna manera incluso estar seguro de que no se trataba de una falla de hardware que no tiene nada que ver con el software.
Evitar problemas de concurrencia debe comenzar durante la fase de diseño, comenzando con exactamente cómo lo hará ("orden de bloqueo global", modelo de actor, ...). No es algo que intentes arreglar con un pánico loco con la esperanza de que todo no se autodestruya después de un próximo lanzamiento.
Tenga en cuenta que no estoy bromeando aquí. Sus propias palabras (" La mayor parte se originó de otros desarrolladores que han abandonado el equipo. Los desarrolladores actuales en el equipo son muy inteligentes, pero en su mayoría junior en términos de experiencia ") indican que toda la experiencia de las personas ya ha hecho lo que yo Estoy sugiriendo.
fuente