¿Qué lecciones aprendiste de un proyecto que casi / realmente fracasó debido a un mal subproceso múltiple? [cerrado]

11

¿Qué lecciones aprendiste de un proyecto que casi / realmente fracasó debido a un mal subproceso múltiple?

A veces, el marco impone un cierto modelo de subprocesos que hace que las cosas en un orden de magnitud sean más difíciles de corregir.

En cuanto a mí, aún no me he recuperado del último fracaso y creo que es mejor para mí no trabajar en nada que tenga que ver con el subprocesamiento múltiple en ese marco.

Descubrí que era bueno en problemas de subprocesos múltiples que tienen una bifurcación / unión simple, y donde los datos solo viajan en una dirección (mientras que las señales pueden viajar en una dirección circular).

No puedo manejar la GUI en la que parte del trabajo solo se puede realizar en un subproceso estrictamente serializado (el "subproceso principal") y otro trabajo solo se puede realizar en cualquier subproceso que no sea el subproceso principal (los "subprocesos de trabajo") y donde los datos y los mensajes tienen que viajar en todas las direcciones entre N componentes (un gráfico completamente conectado).

En el momento en que dejé ese proyecto por otro, había problemas de punto muerto en todas partes. Escuché que 2-3 meses después, varios otros desarrolladores lograron solucionar todos los problemas de punto muerto, hasta el punto de que se pueden enviar a los clientes. Nunca logré descubrir ese conocimiento faltante que me falta.

Algo sobre el proyecto: el número de ID de mensajes (valores enteros que describen el significado de un evento que se puede enviar a la cola de mensajes de otro objeto, independientemente de la secuencia) se encuentra en varios miles. Las cadenas únicas (mensajes de usuario) también se encuentran en aproximadamente mil.

Adicional

La mejor analogía que obtuve de otro equipo (no relacionado con mis proyectos pasados ​​o presentes) fue "poner los datos en una base de datos". ("Base de datos" que se refiere a centralización y actualizaciones atómicas). En una GUI que está fragmentada en múltiples vistas, todas se ejecutan en el mismo "hilo principal" y todo el trabajo pesado no GUI se realiza en hilos de trabajo individuales, los datos de la aplicación deben almacenarse en una sola placa que actúa como una base de datos y dejar que la "base de datos" maneje todas las "actualizaciones atómicas" que involucran dependencias de datos no triviales. Todas las otras partes de la GUI solo manejan el dibujo de la pantalla y nada más. Las partes de la interfaz de usuario podrían almacenar cosas en caché y el usuario no se dará cuenta si está obsoleto por una fracción de segundo, si está diseñado correctamente. Esta "base de datos" también se conoce como "el documento" en la arquitectura de vista de documento. Desafortunadamente, no, mi aplicación realmente almacena todos los datos en las Vistas. No sé por qué fue así.

Compañeros contribuyentes:

(los contribuyentes no necesitan usar ejemplos reales / personales. Las lecciones de ejemplos anecdóticos, si usted lo considera creíble, también son bienvenidas).

rwong
fuente
Creo que ser capaz de 'pensar en hilos' es algo así como un talento y menos algo que se puede aprender, por falta de una mejor redacción. Conozco a muchos desarrolladores que han estado trabajando con sistemas paralelos durante mucho tiempo, pero se ahogan si los datos tienen que ir en más de una dirección.
dauphic

Respuestas:

13

Mi lección favorita: ¡muy ganada! - es que en un programa multiproceso el programador es un cerdo astuto que te odia. Si las cosas pueden salir mal, lo harán, pero de manera inesperada. Si te equivocas, estarás persiguiendo heisenbugs extraños (porque cualquier instrumentación que agregues cambiará los tiempos y te dará un patrón de ejecución diferente).

La única forma sensata de solucionar esto es acorralar estrictamente todo el manejo de hilos en un código tan pequeño que lo haga todo bien y que sea muy conservador para garantizar que los bloqueos se mantengan correctamente (y con un orden de adquisición globalmente constante también) . La forma más fácil de hacerlo es no compartir memoria (u otros recursos) entre subprocesos, excepto los mensajes que deben ser asíncronos; eso te permite escribir todo lo demás en un estilo que no tiene hilos. (Bonificación: escalar a varias máquinas en un clúster es mucho más fácil).

Compañeros de Donal
fuente
+1 para "no compartir memoria (u otros recursos) entre subprocesos, excepto los mensajes que deben ser asíncronos;"
Nemanja Trifunovic
1
La unica manera? ¿Qué pasa con los tipos de datos inmutables?
Aaronaught
is that in a multithreaded program the scheduler is a sneaky swine that hates you.- no, no hace exactamente lo que le dijiste que hiciera :)
mattnz
@Aaronaught: los valores globales pasados ​​por referencia, incluso si son inmutables, aún requieren GC global y eso reintroduce una gran cantidad de recursos globales. Poder usar la administración de memoria por subproceso es bueno, ya que le permite deshacerse de un montón de bloqueos globales.
Donal Fellows
No es que no pueda pasar valores de tipos no básicos por referencia, sino que requiere niveles más altos de bloqueo (por ejemplo, el "propietario" mantiene una referencia hasta que aparezca algún mensaje, lo cual es fácil de estropear en mantenimiento) o código complejo en el motor de mensajería para transferir la propiedad. O reúne todo y desmarca en el otro hilo, que es mucho más lento (de todos modos, debe hacerlo cuando va a un clúster). Ir al grano y no compartir memoria es más fácil.
Donal Fellows
6

Aquí hay algunas lecciones básicas que puedo pensar en este momento (no por proyectos que fallan, sino por problemas reales vistos en proyectos reales):

  • Intente evitar cualquier llamada de bloqueo mientras mantiene un recurso compartido. El patrón de interbloqueo común es el agarre de hilo mutex, hace una devolución de llamada, los bloques de devolución de llamada en el mismo mutex.
  • Proteja el acceso a cualquier estructura de datos compartidos con una sección mutex / crítica (o use las que no requieren bloqueo, ¡pero no invente la suya propia!)
  • No asuma atomicidad: utilice API atómicas (por ejemplo, InterlockedIncrement).
  • RTFM con respecto a la seguridad de subprocesos de bibliotecas, objetos o API que está utilizando.
  • Aproveche las primitivas de sincronización disponibles, por ejemplo, eventos, semáforos. (Pero cuando los use, preste mucha atención porque sabe que está en buen estado; he visto muchos ejemplos de eventos señalados en el estado incorrecto, de modo que los eventos o los datos pueden perderse)
  • Suponga que los subprocesos pueden ejecutarse simultáneamente y / o en cualquier orden y que el contexto puede cambiar entre subprocesos en cualquier momento (a menos que en un sistema operativo que ofrezca otras garantías).
Guy Sirton
fuente
6
  • Todo su proyecto de GUI solo debe llamarse desde el hilo principal . Básicamente, no debe poner una sola "invocación" (.net) en su GUI. El subprocesamiento múltiple debe estar atascado en proyectos separados que manejan el acceso de datos más lento.

Heredamos una parte donde el proyecto GUI está usando una docena de hilos. No está dando nada más que problemas. Puntos muertos, problemas de carrera, llamadas GUI de hilos cruzados ...

Carra
fuente
¿"Proyecto" significa "montaje"? No veo cómo la distribución de clases entre ensamblados podría causar problemas de subprocesos.
Nikkie
En mi proyecto es de hecho una asamblea. Pero el punto principal es que todo el código en esas carpetas debe llamarse desde el hilo principal, sin excepciones.
Carra
No creo que esta regla sea generalmente aplicable. Sí, nunca debe llamar al código GUI desde otro hilo. Pero cómo distribuir clases a carpetas / proyectos / ensamblajes es una decisión independiente.
Nikkie
1

Java 5 y versiones posteriores tienen Ejecutores que están destinados a hacer la vida más fácil para manejar programas de estilo fork-join de subprocesos múltiples.

Úselos, eliminará mucho dolor.

(y, sí, esto lo aprendí de un proyecto :))


fuente
1
Para aplicar esta respuesta a otros idiomas, use marcos de procesamiento paralelo de alta calidad proporcionados por ese idioma siempre que sea posible. (Sin embargo, sólo el tiempo dirá si un marco es muy grande y muy fácil de utilizar.)
rwong
1

Tengo experiencia en sistemas embebidos en tiempo real. No puede probar la ausencia de problemas causados ​​por subprocesos múltiples. (A veces puedes confirmar la presencia). El código tiene que ser demostrablemente correcto. Por lo tanto, las mejores prácticas en torno a cualquier interacción de subprocesos

  • Regla n. ° 1: BESO: si no necesita un hilo, no gire uno. Serializar tanto como sea posible.
  • # 2 regla: no rompa # 1.
  • # 3 Si no puede probar a través de la revisión, es correcto, no lo es.
Mattnz
fuente
+1 para la regla 1. Estaba trabajando en un proyecto que inicialmente se iba a bloquear hasta que se completara otro hilo, ¡esencialmente una llamada al método! Afortunadamente, decidimos en contra de ese enfoque.
Michael K
# 3 FTW. Es mejor pasar horas luchando con los diagramas de sincronización de bloqueo o lo que sea que use para demostrar que es bueno que meses preguntándose por qué a veces se desmorona.
1

Una analogía de una clase sobre multihilo que tomé el año pasado fue muy útil. La sincronización de subprocesos es como una señal de tráfico que protege una intersección (datos) del uso de dos automóviles (subprocesos) a la vez. El error que muchos desarrolladores cometen es encender las luces rojas en la mayor parte de la ciudad para dejar pasar un automóvil porque piensan que es demasiado difícil o peligroso descubrir la señal exacta que necesitan. Eso podría funcionar bien cuando el tráfico es escaso, pero conducirá al estancamiento a medida que su aplicación crezca.

Eso es algo que ya sabía en teoría, pero después de esa clase la analogía realmente me quedó grabada, y me sorprendió la frecuencia con la que investigaría un problema de subprocesos y encontraría una cola gigante, o interrumpiría la desactivación en todas partes durante una escritura en una variable solo se usaron dos subprocesos, o se mantuvieron mutexes durante mucho tiempo cuando se podría refactorizar para evitarlo por completo.

En otras palabras, algunos de los peores problemas de subprocesos son causados ​​por un exceso al tratar de evitar problemas de subprocesos.

Karl Bielefeldt
fuente
0

Intenta hacerlo de nuevo.

Al menos para mí, lo que creó una diferencia fue la práctica. Después de hacer un trabajo multiproceso y distribuido bastantes veces, simplemente le acostumbras.

Creo que la depuración es realmente lo que lo hace difícil. Puedo depurar código de subprocesos múltiples usando VS, pero realmente estoy completamente perdido si tengo que usar gdb. Mi culpa, probablemente.

Otra cosa sobre la que está aprendiendo más es sobre estructuras de datos sin bloqueo.

Creo que esta pregunta se puede mejorar realmente si especifica el marco. Los grupos de subprocesos de .NET y los trabajadores en segundo plano son realmente diferentes a QThread, por ejemplo. Siempre hay algunas trampas específicas de la plataforma.

Vitor Py
fuente
Estoy interesado en escuchar historias de cualquier marco, porque creo que hay cosas que aprender de cada marco, especialmente aquellas a las que no he estado expuesto.
rwong
1
los depuradores son en gran medida inútiles en un entorno multihilo.
Pemdas
Ya tengo rastreadores de ejecución de subprocesos múltiples que me dicen cuál es el problema, pero no me ayudarán a resolverlo. El quid de mi problema es que "según el diseño actual, no puedo pasar el mensaje X al objeto Y de esta manera (secuencia); tiene que agregarse a una cola gigante y eventualmente se procesará; pero debido a esto , no hay forma de que los mensajes aparezcan para el usuario en el momento adecuado: siempre sucederá anacrónicamente y confundirá mucho al usuario . Incluso puede que necesite agregar barras de progreso, botones de cancelación o mensajes de error a lugares que no deberían " tengo esos ".
rwong
0

He aprendido que las devoluciones de llamada de módulos de nivel inferior a módulos de nivel superior son un gran mal porque provocan la adquisición de bloqueos en un orden opuesto.

Sergej Zagursky
fuente
las devoluciones de llamada no son malas ... el hecho de que hagan otra cosa que no sea romper hilos es probablemente la raíz del mal. Sería muy sospechoso de cualquier devolución de llamada que no solo enviara un token a la cola de mensajes.
Pemdas
La resolución de un problema de optimización (como minimizar f (x)) a menudo se implementa proporcionando el puntero a una función f (x) para el procedimiento de optimización, que "lo devuelve" mientras busca el mínimo. ¿Cómo lo harías sin una devolución de llamada?
quant_dev
1
No hay voto negativo, pero las devoluciones de llamada no son malas. Llamar una devolución de llamada mientras mantiene un candado es malo. No llame a nada dentro de una cerradura cuando no sepa si podría cerrarse o esperar. Eso no solo incluye devoluciones de llamada, sino también funciones virtuales, funciones API, funciones en otros módulos ("nivel superior" o "nivel inferior").
nikie
@nikie: si se debe mantener un bloqueo durante la devolución de llamada, el resto de la API debe diseñarse para que sea reentrante (¡difícil!) o el hecho de que esté sosteniendo un bloqueo debe ser una parte documentada de la API ( desafortunado, pero a veces todo lo que puedes hacer).
Donal Fellows
@Donal Fellows: si se debe mantener un bloqueo durante una devolución de llamada, diría que tiene un defecto de diseño. Si realmente no hay otra manera, entonces sí, ¡documentarlo! Tal como documentaría si la devolución de llamada se llamará en un hilo de fondo. Eso es parte de la interfaz.
Nikkie