c multithreading debugging pthreads

Programación en C: depuración con pthreads



multithreading debugging (8)

Cuando comencé a hacer programación multiproceso, dejé de usar los depuradores. Para mí, el punto clave es una buena descomposición y encapsulación del programa.

Los monitores son la forma más fácil de programación multihilo sin errores. Si no puede evitar las dependencias de bloqueo complejas, es fácil verificar si son cíclicas; espere hasta que el programa se cuelgue y verifique los stacktraces usando ''pstack''. Puede romper bloqueos cíclicos introduciendo nuevos subprocesos y buffers de comunicación asíncrona.

Use afirmaciones y asegúrese de escribir pruebas de unidad de una sola secuencia para componentes particulares de su software; luego puede ejecutarlas en el depurador si lo desea.

Una de las cosas más difíciles para mi ajuste inicial fue mi primera experiencia intensa de programación con subprocesos en C. Estaba acostumbrado a saber exactamente cuál sería la siguiente línea de código que se ejecutaría y la mayoría de mis técnicas de depuración se centraron en esa expectativa.

¿Cuáles son algunas buenas técnicas para depurar con pthreads en C? Puede sugerir metodologías personales sin herramientas adicionales, herramientas que use o cualquier otra cosa que lo ayude a depurar.

PD: hago mi programación en C usando gcc en linux, pero no permita que eso restrinja su respuesta necesariamente


En la fase de "pensamiento", antes de comenzar a codificar, use el concepto de Máquina de Estado. Puede hacer el diseño mucho más claro.

Printf puede ayudarlo a comprender la dinámica de su programa. Pero saturan el código fuente, así que usan una macro DEBUG_OUT () y, en su definición, lo habilitan con una bandera booleana. Mejor aún, establezca / borre esta bandera con una señal que envíe a través de ''kill -USR1''. Enviar la salida a un archivo de registro con una marca de tiempo.

también considere usar assert (), y luego analice sus volcados de núcleo usando gdb y ddd.


La depuración de una aplicación multiproceso es difícil. Un buen depurador como GDB (con front-end DDD opcional) para el entorno * nix o el que viene con Visual Studio en Windows ayudará enormemente.


Mi enfoque de la depuración de subprocesos múltiples es similar al de un único subproceso, pero generalmente se gasta más tiempo en la fase de pensamiento:

  1. Desarrollar una teoría sobre lo que podría estar causando el problema.

  2. Determine qué tipo de resultados se pueden esperar si la teoría es cierta.

  3. Si es necesario, agregue un código que pueda refutar o verificar sus resultados y su teoría.

  4. Si tu teoría es cierta, arregla el problema.

A menudo, el "experimento" que prueba la teoría es la adición de una sección crítica o mutex en torno al código sospechoso. Luego trataré de reducir el problema reduciendo sistemáticamente la sección crítica. Las secciones críticas no siempre son la mejor solución (aunque a menudo puede ser la solución rápida). Sin embargo, son útiles para señalar la ''pistola humeante''.

Como dije, los mismos pasos se aplican a la depuración de un solo hilo, aunque es demasiado fácil simplemente saltar a un depurador y tenerlo. La depuración de subprocesos múltiples requiere una comprensión mucho más sólida del código, ya que generalmente encuentro que el código de subprocesos múltiples que se ejecuta a través de un depurador no es útil.

Además, hellgrind es una gran herramienta. El Thread Checker de Intel realiza una función similar para Windows, pero cuesta mucho más que hellgrind.


Prácticamente me desarrollo en un mundo exclusivamente de subprocesos múltiples y alto rendimiento, así que aquí está la práctica general que uso.

Diseño- la mejor optimización es un mejor algoritmo:

1) Divide las funciones en piezas LÓGICAMENTE separables. Esto significa que una llamada hace "A" y SOLO "A", no A, luego B, luego C ...
2) SIN EFECTOS SECUNDARIOS: elimine todas las variables globales, estáticas o no. Si no puede eliminar por completo los efectos secundarios, aislelos en algunas ubicaciones (concéntrelos en el código).
3) Haga tantos componentes aislados como RE-ENTRANTES como sea posible. Esto significa que no tienen estado: toman todas sus entradas como constantes y solo manipulan DECLARADOS, parámetros lógicamente constantes para producir la salida. Pase por valor en lugar de referencia siempre que pueda.
4) Si tiene estado, haga una separación clara entre los subconjuntos sin estado y la máquina de estado real. Idealmente, la máquina de estado será una sola función o clase que manipule componentes sin estado.

Depuración:

Insectos de subprocesamiento tienden a venir en 2 amplios sabores: razas y puntos muertos. Como regla general, los puntos muertos son mucho más deterministas.

1) ¿Ve corrupción de datos ?: SÍ => Probablemente una raza.
2) ¿Surge el error en CADA carrera o solo en algunas carreras ?: SÍ => Probablemente un punto muerto (las razas generalmente no son deterministas).
3) ¿Alguna vez se cuelga el proceso ?: SÍ => Hay un punto muerto en alguna parte. Si solo se cuelga a veces, es probable que también tengas una carrera.

Los puntos de interrupción a menudo actúan como primitivas de sincronización MISMOS en el código, porque son lógicamente similares: obligan a la ejecución a detenerse en el contexto actual hasta que algún otro contexto (usted) envíe una señal para reanudarse. Esto significa que debe ver cualquier punto de interrupción que tenga en el código que altere su comportamiento de subprocesos múltiples, y los puntos de interrupción afectarán las condiciones de la carrera pero (en general) no los puntos muertos.

Como regla general, esto significa que debe eliminar todos los puntos de interrupción, identificar el tipo de error, y luego reintroducirlos para tratar de solucionarlo. De lo contrario, simplemente distorsionan las cosas aún más.


Tiendo a usar muchos puntos de interrupción. Si realmente no te importa la función del hilo, pero sí te preocupes por sus efectos secundarios, un buen momento para verificarlos podría ser justo antes de que salga o vuelva a su estado de espera o lo que sea que esté haciendo.


Una de las cosas que le sorprenderán acerca de la depuración de programas de subprocesos es que a menudo encontrará los cambios de error, o incluso desaparece cuando agrega printf o ejecuta el programa en el depurador (conocido coloquialmente como Heisenbug ).

En un programa con hilos, un Heisenbug generalmente significa que tienes una condición de carrera . Un buen programador buscará variables compartidas o recursos que dependen de la orden. Un programador de mierda intentará arreglarlo a ciegas con declaraciones de sleep ().


Valgrind es una excelente herramienta para encontrar condiciones de carrera y mal uso de API. Mantiene un modelo de acceso a la memoria del programa (y quizás de los recursos compartidos) y detectará bloqueos faltantes incluso cuando el error sea benigno (lo que, por supuesto, significa que se volverá menos inesperadamente completamente inesperado en algún momento posterior).

Para usarlo, valgrind --tool=helgrind , aquí está su manual . Además, hay valgrind --tool=drd ( manual ). Helgrind y DRD utilizan diferentes modelos para detectar superposiciones pero posiblemente diferentes conjuntos de errores. También pueden ocurrir falsos positivos.

De todos modos, valgrind ha ahorrado innumerables horas de depuración (aunque no todas): para mí.