← Concurrencia y Sincronización Bloque 4 · Mutex · Tema 19 de 47

Tema 19

Detectar carreras con herramientas

Las condiciones de carrera son no deterministas: pueden no aparecer en las pruebas y aparecer solo en producción. Las herramientas de análisis dinámico (ThreadSanitizer, Helgrind, DRD) detectan carreras y mal uso de mutex automáticamente, sin necesidad de reproducir el error. En este tema verás cómo funcionan, cómo se usan y cómo interpretar sus informes.

1. Por qué necesitamos herramientas

Las condiciones de carrera son el error más difícil de encontrar en programas concurrentes porque son no deterministas: dependen del intercalado exacto de las instrucciones, que cambia en cada ejecución. Un programa puede pasar mil pruebas y fallar en la mil uno.

Las pruebas tradicionales (unit tests, integration tests) no son suficientes porque solo verifican los intercalados que ocurren durante la prueba. Los intercalados que causan errores pueden no ocurrir durante las pruebas y aparecer solo en producción, con carga.

Las herramientas de análisis dinámico resuelven este problema: instrumentan el programa para detectar carreras en tiempo de ejecución, independientemente de si ocurren o no. Si un acceso concurrente a un dato compartido es posible, la herramienta lo detecta.

Las herramientas son necesarias, no opcionales

En programas concurrentes, las pruebas no son suficientes. Las herramientas de análisis dinámico son necesarias para detectar carreras que las pruebas no pueden encontrar.

2. ThreadSanitizer (TSan)

ThreadSanitizer (TSan) es una herramienta de Google que detecta condiciones de carrera y otros errores de concurrencia en programas C/C++. Se integra en el compilador (GCC y Clang) como un sanitizer: se activa con la flag -fsanitize=thread.

TSan detecta:

TSan es la herramienta más usada en la práctica para detectar carreras en C/C++. Es rápida (overhead de 2-5×), precisa (pocos falsos positivos) y fácil de usar.

3. Cómo funciona TSan

TSan instrumenta el código en tiempo de compilación: añade código que registra cada acceso a memoria (lectura y escritura) y cada operación de sincronización (lock, unlock, atomic). En tiempo de ejecución, TSan mantiene una tabla de «shadow memory» que registra el estado de cada byte de memoria.

Cuando un hilo accede a una variable, TSan verifica si otro hilo ha accedido a la misma variable sin sincronización. Si es así, reporta una carrera.

La shadow memory tiene un overhead de memoria: por cada byte de memoria del programa, TSan usa 8 bytes de shadow memory. Esto puede ser un problema para programas con mucha memoria.

TSan también usa vector clocks para rastrear el orden parcial de las operaciones (happens-before). Esto le permite determinar si dos accesos están sincronizados o no.

4. Usar TSan

Para usar TSan, se compila con -fsanitize=thread y se ejecuta normalmente:

terminal · compilar y ejecutar con TSan
# Compilar con TSan
$ gcc -std=c17 -Wall -Wextra -pthread -fsanitize=thread -g -o racer_tsan racer.c

# Ejecutar (TSan detecta la carrera automáticamente)
$ ./racer_tsan 10000 4
==================
WARNING: ThreadSanitizer: data race (pid=12345)
  Write of size 4 at 0x55a3b2c3d4e0 by thread T2:
    #0 incrementar racer.c:12
    #1 (anonymous) racer.c:20

  Previous read of size 4 at 0x55a3b2c3d4e0 by thread T1:
    #0 incrementar racer.c:12
    #1 (anonymous) racer.c:19

  Location is global 'counter' of size 4 at 0x55a3b2c3d4e0 (racer.c:8)

  Thread T1 (tid=12346, running) created by main thread at:
    #0 pthread_create
    #1 main racer.c:19

  Thread T2 (tid=12347, running) created by main thread at:
    #0 pthread_create
    #1 main racer.c:20
==================
ThreadSanitizer: reported 1 warnings

TSan imprime un informe detallado: el tipo de acceso (lectura/escritura), el tamaño, la dirección de memoria, el stack trace de ambos accesos, la ubicación del dato y la creación de los hilos.

5. Interpretar el informe de TSan

El informe de TSan tiene estas secciones:

TSan también reporta otros errores:

6. Helgrind

Helgrind es una herramienta de Valgrind que detecta condiciones de carrera y errores de sincronización en programas C/C++. A diferencia de TSan, no requiere recompilación: se ejecuta sobre el binario existente.

Helgrind detecta:

Helgrind es más lento que TSan (overhead de 20-50×) pero no requiere recompilación y detecta más tipos de errores.

7. Cómo funciona Helgrind

Helgrind usa la técnica de happens-before: mantiene un grafo de dependencias entre operaciones y verifica que los accesos concurrentes estén ordenados por happens-before. Si dos accesos al mismo dato no están ordenados por happens-before, es una carrera.

Helgrind también usa locksets: para cada acceso a memoria, registra qué locks están bloqueados. Si dos accesos al mismo dato no comparten un lock común, es una posible carrera.

Helgrind es más conservador que TSan: puede reportar falsos positivos (carreras que no existen) porque usa el lockset analysis, que es menos preciso que el happens-before de TSan.

8. Usar Helgrind

Para usar Helgrind, se compila normalmente (con -g para debug) y se ejecuta con Valgrind:

terminal · Helgrind
# Compilar con debug
$ gcc -std=c17 -Wall -Wextra -pthread -g -o racer racer.c

# Ejecutar con Helgrind
$ valgrind --tool=helgrind ./racer 10000 4
==12345== Helgrind, a thread error detector
==12345== Possible data race during read of size 4 at 0x...
==12345==    at 0x...: incrementar (racer.c:12)
==12345==    by 0x...: start_thread
==12345==
==12345== This conflicts with a previous write of size 4 at 0x...
==12345==    at 0x...: incrementar (racer.c:12)
==12345==    by 0x...: start_thread
==12345==
==12345== Locks held: none
==12345==

Helgrind reporta la carrera y el stack trace de ambos accesos. También reporta «Locks held: none», indicando que no había ningún lock protegiendo el acceso.

9. DRD

DRD (Data Race Detector) es otra herramienta de Valgrind, más rápida que Helgrind pero menos completa. Usa un enfoque similar al happens-before pero con menos overhead.

DRD detecta:

DRD no detecta orden de adquisición de locks (posibles deadlocks) ni problemas con variables de condición. Es más rápido que Helgrind (overhead de 10-20× vs. 20-50×) pero menos completo.

terminal · DRD
$ valgrind --tool=drd ./racer 10000 4
==12345== DRD, a thread error detector
==12345== Conflicting load by thread 2 at 0x... size 4
==12345==    at 0x...: incrementar (racer.c:12)
==12345== Conflicting store by thread 1 at 0x... size 4
==12345==    at 0x...: incrementar (racer.c:12)
==12345==

10. Comparación de herramientas

HerramientaOverheadRequiere recompilaciónFalsos positivosDetecta
ThreadSanitizer2-5× (tiempo), 5-10× (memoria)Sí (-fsanitize=thread)PocosCarreras, mal uso de mutex, thread leaks.
Helgrind20-50×NoAlgunosCarreras, mal uso de mutex, orden de locks, variables de condición.
DRD10-20×NoPocosCarreras, mal uso de mutex.

La recomendación es:

11. Limitaciones

Las herramientas de análisis dinámico tienen limitaciones:

Por eso, las herramientas son complementarias a las buenas prácticas de programación: no reemplazan el diseño cuidadoso.

12. Simulador interactivo

Este simulador muestra cómo ThreadSanitizer detecta una carrera:

Simulador · detección de carreras

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es ThreadSanitizer?

Una herramienta de Google que detecta carreras de datos y mal uso de mutex en C/C++.

2. ¿Cómo se activa TSan?

Con la flag -fsanitize=thread al compilar.

3. ¿Qué detecta TSan?

Carreras de datos, mal uso de mutex, thread leaks, destrucción de mutex bloqueado.

4. ¿Qué es Helgrind?

Una herramienta de Valgrind que detecta carreras y errores de sincronización sin recompilación.

5. ¿Qué técnica usa Helgrind?

Happens-before y lockset analysis.

6. ¿Qué es DRD?

Otra herramienta de Valgrind, más rápida que Helgrind pero menos completa.

7. ¿Cuál es el overhead de TSan?

2-5× en tiempo, 5-10× en memoria.

8. ¿Cuál es el overhead de Helgrind?

20-50×.

9. ¿Qué limitación tienen las herramientas de análisis dinámico?

Solo detectan lo que se ejecuta. Si un código con carrera no se ejecuta, no se detecta.

10. ¿Cuál es la herramienta recomendada para desarrollo?

ThreadSanitizer.

Para practicar

  1. Compila racer.c con -fsanitize=thread y ejecuta. ¿Qué reporta TSan?
  2. Compila mutex_counter.c con -fsanitize=thread y verifica que no hay carreras.
  3. Ejecuta racer con Helgrind y compara el informe con TSan.
  4. Ejecuta racer con DRD y compara el rendimiento con Helgrind.
  5. Escribe un programa con un double lock y verifica que TSan lo detecta.
  6. Investiga cómo se integran las herramientas en un pipeline de CI/CD.