¿Qué buscas al depurar puntos muertos?

25

Recientemente he estado trabajando en proyectos que utilizan mucho el subproceso. Creo que estoy bien en diseñarlos; utilizar el diseño sin estado tanto como sea posible, bloquear el acceso a todos los recursos que más de un hilo necesita, etc. Mi experiencia en programación funcional me ha ayudado enormemente.

Sin embargo, cuando leo el código de hilo de otras personas, me confundo. Estoy depurando un punto muerto en este momento, y dado que el estilo y el diseño de la codificación son diferentes de mi estilo personal, estoy teniendo dificultades para ver las posibles condiciones de punto muerto.

¿Qué buscas al depurar puntos muertos?

Michael K
fuente
Estoy preguntando esto aquí en lugar de SO porque quiero indicadores más generales sobre la depuración de puntos muertos, no una respuesta específica a mi problema.
Michael K
Las estrategias en las que puedo pensar son el registro (como han señalado varios otros), en realidad examinando el gráfico de punto muerto de quién está esperando un candado retenido por quién (ver stackoverflow.com/questions/3483094/... para algunos punteros) y anotaciones de bloqueo (consulte clang.llvm.org/docs/ThreadSafetyAnalysis.html ). Incluso si no es su código, puede intentar convencer al autor para que agregue anotaciones; probablemente encontrarán errores y los corregirán (posiblemente incluyendo el suyo) en el proceso.
Don Hatch

Respuestas:

23

Si la situación es un punto muerto real (es decir, dos subprocesos tienen dos bloqueos diferentes, pero al menos un subproceso quiere un bloqueo que retenga el otro subproceso), primero debe abandonar todas las concepciones previas de cómo los subprocesos ordenan el bloqueo. Asumir nada. Es posible que desee eliminar todos los comentarios del código que está viendo, ya que esos comentarios pueden hacer que crea algo que no es cierto. Es difícil enfatizar esto lo suficiente: no asumas nada.

Después de eso, determine qué bloqueos se mantienen mientras un hilo intenta bloquear algo más. Si puede, asegúrese de que un hilo se desbloquee en orden inverso al bloqueo. Aún mejor, asegúrese de que un hilo contenga solo un bloqueo a la vez.

Trabaje minuciosamente a través de la ejecución de un hilo y examine todos los eventos de bloqueo. En cada bloqueo, determine si un subproceso contiene otros bloqueos y, de ser así, en qué circunstancias otro subproceso, que realiza una ruta de ejecución similar, puede llegar al evento de bloqueo en consideración.

Es ciertamente posible que no encuentre el problema antes de que se le acabe el tiempo o el dinero.

Bruce Ediger
fuente
44
+1 Wow, eso es pesimista ... no es verdad, sin embargo. Es un hecho que no puedes encontrar todos los errores. Gracias por las sugerencias!
Michael K
Bruce, tu caracterización del "punto muerto real" me sorprende. Pensé que un punto muerto entre dos hilos es cuando cada uno está esperando un bloqueo que el otro tiene. Su definición también parece incluir el caso de que un subproceso, mientras mantiene un bloqueo, espera adquirir un segundo bloqueo que actualmente está retenido por un subproceso diferente. Eso no me parece un punto muerto; ¿¿Lo es??
Don Hatch
@DonHatch - Lo expresé mal. La situación que describe no es un punto muerto. Tenía la esperanza de transmitir el desorden de depurar una situación que incluye un bloqueo de retención de hilo A, luego tratar de adquirir el bloqueo B, mientras que el hilo que mantiene el bloqueo B está tratando de adquirir el bloqueo A. Quizás. O tal vez la situación es mucho más complicada. Solo necesita tener una mente muy abierta sobre el orden de adquisición de la cerradura. Examina todos los supuestos. No confíes en nada.
Bruce Ediger
+1 sugiere leer cuidadosamente el código y examinar todas las operaciones de bloqueo de forma aislada. Es mucho más fácil mirar un gráfico complejo al examinar cuidadosamente un solo nodo que tratar de ver todo de una vez. ¿Cuántas veces he encontrado el problema simplemente mirando el código y ejecutando diferentes escenarios en mi cabeza?
Newtopian
11
  1. Como otros han dicho ... si puede obtener información útil para iniciar sesión, intente eso primero, ya que es lo más fácil de hacer.

  2. Identifique las cerraduras que están involucradas. Cambie todos los mutex / semáforos que esperan para siempre a esperas programadas ... algo ridículamente largo como 5 minutos. Registre el error cuando se agote el tiempo de espera. Esto al menos lo guiará en la dirección de uno de los bloqueos involucrados en el problema. Dependiendo de la variabilidad del tiempo, puede tener suerte y encontrar ambos bloqueos después de unas pocas carreras. Use las condiciones / códigos de falla de la función para registrar un seguimiento de pseudoapila después de que la espera temporizada no identifique cómo llegó allí en primer lugar. Esto debería ayudarlo a identificar el hilo que está involucrado en el problema.

  3. Otra cosa que podría intentar es construir una biblioteca de envoltura alrededor de sus servicios de mutex / semáforo. Rastree qué hilos tienen cada mutex y qué hilos están esperando en el mutex. Cree un hilo de monitor que verifique cuánto tiempo los hilos han estado bloqueando. Active una duración razonable y descargue la información de estado que está rastreando.

En algún momento, será necesaria una simple inspección de código antiguo.

Pemdas
fuente
6

El primer paso (como dice Péter) es iniciar sesión. Aunque en mi experiencia esto a menudo es problemático. En el procesamiento paralelo pesado, esto a menudo no es posible. Tuve que depurar algo similar con una red neuronal una vez, que procesaba 100k de nodos por segundo. El error ocurrió solo después de varias horas e incluso una sola línea de salida desaceleró tanto las cosas que habría llevado días. Si es posible iniciar sesión, concéntrese menos en los datos, pero más en el flujo del programa, hasta que sepa en qué parte sucede. Solo una línea simple al comienzo de cada función y, si puede encontrar la función correcta, divídala en fragmentos más pequeños.

Otra opción es eliminar partes del código y los datos para localizar el error. Tal vez incluso escriba algún pequeño programa que tome solo algunas de las clases y ejecute solo las pruebas más básicas (todavía en varios hilos, por supuesto). Elimine todo lo relacionado con la interfaz gráfica de usuario, por ejemplo, cualquier resultado sobre el estado de procesamiento real. (Me pareció que la interfaz de usuario es la fuente del error con bastante frecuencia)

En su código, intente seguir el flujo lógico completo de control entre inicializar el bloqueo y liberarlo. Un error común podría ser bloquear al comienzo de una función, desbloquear al final, pero tener una declaración de retorno condicional en algún punto intermedio. Las excepciones también podrían evitar la liberación.

Thorsten Müller
fuente
"Las excepciones podrían impedir la publicación" -> Me da pena los idiomas que no tienen variables de ámbito: /
Matthieu M.
1
@Matthieu: Tener variables de alcance y usarlas adecuadamente puede ser dos cosas diferentes. Y pidió posibles problemas en general, sin mencionar un idioma específico. Entonces, esto es una cosa, que puede influir en el flujo de control.
thorsten müller
3

Mis mejores amigos han sido declaraciones de impresión / registro en lugares interesantes dentro del código. Esto generalmente me ayuda a comprender mejor lo que realmente está sucediendo dentro de la aplicación, sin interrumpir el tiempo entre diferentes hilos, lo que podría evitar la reproducción del error.

Si eso falla, mi único método restante es mirar el código e intentar construir un modelo mental de los diversos hilos e interacciones, y tratar de pensar en posibles formas locas de lograr lo que aparentemente ha sucedido :-) Pero no lo hago me considero un asesino de estancamientos muy experimentado. Esperemos que otros puedan dar mejores ideas, de las cuales yo también pueda aprender :-)

Péter Török
fuente
1
Hoy depuré un par de cerraduras muertas como esta. El truco consistía en envolver pthread_mutex_lock () con una macro que imprime la función, el número de línea, el nombre del archivo y el nombre de la variable mutex (tokenizando) antes y después de adquirir el bloqueo. Haga lo mismo para pthread_mutex_unlock () también. Cuando vi que mi hilo se congeló, solo tuve que mirar los dos últimos mensajes, ¡había dos hilos que intentaban bloquear pero nunca lo terminaban! Ahora todo lo que queda es agregar un mecanismo para alternar esto en tiempo de ejecución. :-)
Plumenator
3

En primer lugar, intente obtener el autor de ese código. Probablemente tendrá la idea de lo que había escrito. incluso si ustedes dos no pueden identificar el problema con solo hablar, al menos pueden sentarse con él para identificar la porción de punto muerto, que será mucho más rápido de lo que entienden su código sin ayuda.

De lo contrario, como dijo Péter Török, el registro probablemente sea el camino. Hasta donde yo sé, Debugger hizo un mal trabajo en un entorno de subprocesos múltiples. trate de ubicar dónde está la cerradura, obtenga un conjunto de los recursos que están esperando y en qué condición se produce la condición de carrera.

Zekta Chan
fuente
no, el registro es tu enemigo aquí: cuando pones un inicio de sesión lento, cambias el comportamiento del programa hasta el punto en que es fácil obtener un programa que funcione perfectamente bien con el registro habilitado, pero se interrumpe cuando el registro está apagado. Es el mismo tipo de problema que tendría al ejecutar un programa en una sola CPU en lugar de en una CPU multinúcleo.
gbjbaanb
@gbjbaanb, creo que decir que es tu enemigo es demasiado duro. Quizás sería correcto decir que es tu mejor amigo, que te decepciona de vez en cuando. Estoy de acuerdo con varias otras personas en esta página que dicen que el registro es un buen primer paso a seguir, después de que el examen del código haya fallado; a menudo (de hecho, la mayoría del tiempo, según mi experiencia) una simple estrategia de registro localizará el problema fácilmente, y ya está. De lo contrario, recurra a otros métodos, pero no creo que sea un buen consejo evitar probar cuál es la mejor herramienta para el trabajo porque no siempre es útil.
Don Hatch
0

Esta pregunta me atrae;) En primer lugar, considérese afortunado ya que pudo reproducir el problema de manera consistente en cada ejecución. Si recibe la misma excepción con el mismo stacktrace cada vez, entonces debería ser bastante sencillo. Si no es así, entonces no confíe tanto en el stacktrace, solo monitoree el acceso a los objetos globales y sus cambios de estado durante la ejecución.


fuente
0

Si tiene que depurar puntos muertos, ya está en problemas. Como regla general, use cerraduras por el menor tiempo posible, o no use ninguno, si es posible. Debe evitarse cualquier situación en la que bloquee y luego pase a un código no trivial.

Por supuesto, depende de su entorno de programación, pero debe mirar cosas como colas secuenciales que pueden permitirle acceder a un recurso desde un solo hilo.

Y luego hay una estrategia antigua pero infalible: Asigna un "nivel" a cada cerradura, comenzando en el nivel 0. Si tomas una cerradura de nivel 0, no tienes permitido ninguna otra cerradura. Después de tomar un bloqueo de nivel 1, puede tomar un bloqueo de nivel 0. Después de tomar un candado de nivel 10, puede tomar candados en los niveles 9 o inferiores, etc.

Si le resulta imposible hacerlo, debe corregir su código porque se encontrará con puntos muertos.

gnasher729
fuente