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.
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:
- Carreras de datos (data races): dos hilos acceden al mismo dato, al menos uno lo modifica, y no hay sincronización.
- Uso incorrecto de mutex: desbloquear un mutex no bloqueado, bloquear dos veces, etc.
- Destrucción de mutex bloqueado: destruir un mutex que está bloqueado.
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:
# 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:
- Tipo de carrera: «data race» (carrera de datos) o «data race on vptr» (carrera en un puntero virtual, típico de C++).
- Acceso 1: el tipo de acceso (Write/Read), el tamaño, la dirección y el stack trace.
- Acceso 2: el otro acceso conflictivo.
- Ubicación: dónde está el dato (global, heap, stack).
- Hilos: información sobre los hilos involucrados (creación, estado).
TSan también reporta otros errores:
- lock-order-inversion: posible deadlock por orden de adquisición de locks.
- thread leak: un hilo no se unió (pthread_join no llamado).
- mutex destroy locked: se destruyó un mutex que estaba bloqueado.
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:
- Carreras de datos: igual que TSan.
- Mal uso de mutex: bloquear dos veces, desbloquear sin bloquear, destruir bloqueado.
- Orden de adquisición de locks: detecta posibles deadlocks por orden de adquisición.
- Variables de condición: mal uso de pthread_cond_wait y pthread_cond_signal.
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:
# 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:
- Carreras de datos.
- Mal uso de mutex (bloqueo dos veces, desbloqueo sin bloquear).
- Destrucción de mutex bloqueado.
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.
$ 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
| Herramienta | Overhead | Requiere recompilación | Falsos positivos | Detecta |
|---|---|---|---|---|
| ThreadSanitizer | 2-5× (tiempo), 5-10× (memoria) | Sí (-fsanitize=thread) | Pocos | Carreras, mal uso de mutex, thread leaks. |
| Helgrind | 20-50× | No | Algunos | Carreras, mal uso de mutex, orden de locks, variables de condición. |
| DRD | 10-20× | No | Pocos | Carreras, mal uso de mutex. |
La recomendación es:
- Desarrollo: usar ThreadSanitizer (más rápido, más preciso).
- Producción (testing): usar Helgrind o DRD (no requieren recompilación).
- CI/CD: usar ThreadSanitizer en los tests automatizados.
11. Limitaciones
Las herramientas de análisis dinámico tienen limitaciones:
- Solo detectan lo que se ejecuta: si un código con carrera no se ejecuta durante la prueba, no se detecta.
- Overhead: el programa es más lento, lo que puede enmascarar carreras que dependen del timing.
- Falsos positivos: especialmente Helgrind, que puede reportar carreras que no existen.
- Falsos negativos: pueden no detectar carreras en código que no se ejecuta.
- Memoria: TSan usa mucha memoria (shadow memory).
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
- ThreadSanitizer detecta carreras y mal uso de mutex en tiempo de ejecución.
- Se activa con -fsanitize=thread y requiere recompilación.
- Helgrind detecta carreras y mal uso de mutex sin recompilación.
- DRD es más rápido que Helgrind pero menos completo.
- TSan es la herramienta recomendada para desarrollo.
- Helgrind y DRD son útiles para testing sin recompilación.
- Las herramientas solo detectan lo que se ejecuta.
- Las herramientas son complementarias a las buenas prácticas.
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
- Compila racer.c con -fsanitize=thread y ejecuta. ¿Qué reporta TSan?
- Compila mutex_counter.c con -fsanitize=thread y verifica que no hay carreras.
- Ejecuta racer con Helgrind y compara el informe con TSan.
- Ejecuta racer con DRD y compara el rendimiento con Helgrind.
- Escribe un programa con un double lock y verifica que TSan lo detecta.
- Investiga cómo se integran las herramientas en un pipeline de CI/CD.