Plagado de errores multiproceso

26

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:

  1. 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 .

  2. 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)

  3. Educar a su equipo de desarrollo sobre los problemas con el código multiproceso.

  4. ¿Qué harías?

koncurrency
fuente
23
Algunas personas, cuando se enfrentan a un problema, piensan que usaré subprocesos múltiples. Ahora tienen sondas twolm
Tom Squires
20
lo que funciona bien para mí es deshacerme de la mutabilidad siempre que sea posible. Cuando veo que el objeto mutable cambia de estado para pasar el nuevo valor, trato de refactorizarlo para que pase un nuevo objeto inmutable que tenga un valor cambiado. Si la inicialización del objeto se realiza de forma segura, lo que garantiza la ausencia de carreras de datos - un alivio
mosquito
3
Bienvenido al infierno de múltiples hilos. Llevo 20 años escribiendo programas multihilo / paralelos en Ada, Occam, C ++. Nunca es fácil, todo requiere un pensamiento muy cuidadoso, y cualquiera que diga "es fácil hacer X" es un tonto que realmente no entiende lo que está sucediendo. Buena suerte.
rapid_now
2
Si quieres ver la concurrencia bien hecha, ¡usa Erlang! En verdad, lo que quieres es una forma de modelo de actor compartido donde se eliminen los casos extraños.
Zachary K
3
@DeadMG Yo diría que la concurrencia de estado compartida es inherentemente propensa a casos extraños de esquina y debe evitarse. Pero hey, escribí un libro sobre Erlang
Zachary K

Respuestas:

27

Su código tiene otros problemas importantes además de eso. ¿Eliminar manualmente un puntero? ¿Llamando a una cleanupfunció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 se DoSomethingImportantlanza 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.

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.

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.

DeadMG
fuente
Esta es una respuesta correcta, pero no es la dirección que estaba buscando, porque pasa demasiado tiempo evaluando un fragmento de código de muestra que fue escrito solo por simplicidad (y no refleja nuestro código real en nuestro producto). Pero tengo curiosidad sobre un comentario que hizo: "herramientas poco confiables". ¿Qué es una herramienta poco confiable? ¿Qué herramientas me recomiendan?
koncurrency
55
@koncurrency: una herramienta poco confiable es algo como la gestión manual de la memoria o escribir su propia sincronización donde, en teoría, resuelve un problema X, pero en realidad es tan malo que puede garantizar errores gigantes y la única forma en que podría resolver el problema a mano en una escala razonable es por la inversión masiva y desproporcionada del tiempo del desarrollador, que es lo que tienes ahora.
DeadMG
9

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.

Seth Noble
fuente
+1 para una sugerencia razonable e intermedia sobre cómo superar el desafío existente.
Sí, este es el enfoque que estoy investigando. Planteas buenos puntos sobre el rendimiento.
koncurrency
Cambiar las cosas para pasar por un solo hilo de envío no suena como un parche rápido, sino más bien un refactor masivo para mí.
Sebastian Redl
8

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.

Kevin Cline
fuente
Acabo de leer esto ahora, después de que mi respuesta fue votada. Solo quería decir que me encanta la frase introductoria :)
back2dos
7

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

  1. Recibir mensaje
  2. Realizar cómputo
  3. Enviar mensaje (s) / crear / matar a otros actores.

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.

Giorgio
fuente
1
La biblioteca Akka para Scala es una buena implementación de esto, que piensa mucho sobre cómo matar a los padres padres cuando los niños mueren, o viceversa. Sé que no es C ++, pero vale la pena echarle un vistazo: akka.io
GlenPeterson
1
@GlenPeterson: Gracias, sé sobre akka (que considero la solución más interesante en este momento y funciona tanto con Java como con Scala), pero la pregunta aborda específicamente C ++. De lo contrario, uno podría considerar Erlang. Supongo que en Erlang todos los dolores de cabeza de la programación multihilo se han ido para siempre. Pero tal vez los marcos como akka se acercan mucho.
Giorgio
"Supongo que en Erlang todos los dolores de cabeza de la programación multihilo se han ido para siempre". Creo que tal vez esto es un poco exagerado. O si es cierto, entonces puede faltar el rendimiento. Sé que Akka no funciona con C ++, solo digo que parece un estado de la técnica para administrar múltiples hilos. Sin embargo, no es seguro para subprocesos. Todavía puedes pasar un estado mutable entre actores y dispararte en el pie.
GlenPeterson
No soy un experto en Erlang, pero AFAIK cada actor se ejecuta de forma aislada y se intercambian mensajes inmutables. Por lo tanto, realmente no tiene que lidiar con hilos y estado mutable compartido en absoluto. El rendimiento es probablemente más bajo que C ++, pero esto siempre sucede cuando aumenta el nivel de abstracción (aumenta el tiempo de ejecución pero reduce el tiempo de desarrollo).
Giorgio
¿Puede el votante dejar un comentario y sugerirme cómo puedo mejorar esta respuesta?
Giorgio el
6

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:

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 Fooes 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.

back2dos
fuente
2

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

  • Pase objetos inmutables o haga copias de ellos antes de pasar.
  • 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).

Tim Williscroft
fuente
2

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 ).

Erhannis
fuente
1

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.

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

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)

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.

Educar a su equipo de desarrollo sobre los problemas con el código multiproceso.

El libro ayudaría, pero creo que los ejercicios / prototipos y el mentor experimentado serían mejores.

¿Qué harías?

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.

Klaim
fuente
Gracias por la sugerencia del libro. Probablemente lo recogeré.
koncurrency
Enhebrar es realmente difícil. No todos los programadores están a la altura del desafío. En el mundo de los negocios, cada vez que veía que se usaban hilos, estaban rodeados de cerraduras de tal manera que no podían ejecutarse dos hilos al mismo tiempo. Hay reglas que puede seguir para hacerlo más fácil, pero aún es difícil.
GlenPeterson
@GlenPeterson De acuerdo, ahora que tengo más experiencia (desde esta respuesta) encuentro que necesitamos mejores abstracciones para que sea manejable y desaliente el intercambio de datos. Afortunadamente, los diseñadores de idiomas parecen trabajar duro en esto.
Klaim
Estoy realmente impresionado con Scala, específicamente por traer beneficios de programación funcional de inmutabilidad, efectos secundarios mínimos a Java, que es un descendiente directo de C ++. Se ejecuta en la máquina virtual Java, por lo que es posible que no tenga el rendimiento que necesita. El libro de Joshua Bloch, "Java efectivo" se trata de minimizar la mutabilidad, crear interfaces herméticas y la seguridad de los hilos. Aunque está basado en Java, apuesto a que podría aplicar el 80-90% de él a C ++. Cuestionar la mutabilidad y el estado compartido (o la mutabilidad del estado compartido) en sus revisiones de código podría ser un buen primer paso para usted.
GlenPeterson
1

Ya está en camino al reconocer el problema y buscar activamente una solución. Esto es lo que haría:

  • Siéntese y diseñe un modelo de subprocesos para su aplicación. Este es un documento que responde preguntas como: ¿Qué tipos de hilos tiene? ¿Qué cosas se deben hacer en qué hilo? ¿Qué tipos diferentes de patrones de sincronización debería usar? En otras palabras, debe describir las "reglas de compromiso" cuando se enfrentan problemas de subprocesos múltiples.
  • Use herramientas de análisis de subprocesos para verificar su base de código en busca de errores. Valgrind tiene un verificador de hilos llamado Helgrind que es bueno para detectar cosas como el estado compartido que se manipula sin una sincronización adecuada. Sin duda hay otras buenas herramientas por ahí, ve a buscarlas.
  • Considere migrar lejos de C ++. C ++ es una pesadilla para escribir programas concurrentes. Mi elección personal sería Erlang , pero eso es cuestión de gustos.
JesperE
fuente
8
Definitivamente -1 para el último bit. Parece que el código del OP está utilizando las herramientas más primitivas y no las herramientas reales de C ++.
DeadMG
2
No estoy de acuerdo La concurrencia en C ++ es una pesadilla, incluso si utiliza los mecanismos y herramientas de C ++ adecuados. Y tenga en cuenta que elegí la frase " considerar ". Entiendo completamente que puede no ser una alternativa realista, pero quedarse con C ++ sin considerar las alternativas es simplemente una tontería.
JesperE
44
@JesperE: lo siento, pero no. La concurrencia en C ++ es solo una pesadilla si la logras yendo a un nivel demasiado bajo. Utilice una abstracción de subprocesos adecuada y no es peor que cualquier otro lenguaje o tiempo de ejecución. Y con una estructura de aplicación adecuada, en realidad es tan fácil como cualquier otra cosa que haya visto.
Michael Kohne
2
Cuando trabajo, creo que tenemos una estructura de aplicación adecuada, utilizamos las abstracciones de subprocesos correctas, etc. A pesar de esto, hemos pasado innumerables horas a lo largo de los años depurando errores que simplemente no aparecerían en idiomas correctamente diseñados para la concurrencia. Pero tengo la sensación de que tendremos que aceptar no estar de acuerdo con esto.
JesperE
1
@JesperE: estoy de acuerdo contigo. El modelo Erlang (para el que existen implementaciones para Scala / Java, Ruby y, que yo sepa, también para C ++) es mucho más robusto que la codificación directa con hilos.
Giorgio
1

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.

gnasher729
fuente
0

¿Qué harías?

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.

Brendan
fuente