CodeGym /Cursos /C# SELF /Race Condition (estado de carrera)

Race Condition (estado de carrera)

C# SELF
Nivel 55 , Lección 4
Disponible

1. Introducción

En aplicaciones multihilo la presencia de un race condition — es una cuestión no de "si", sino de "cuándo ocurrirá". Aunque pienses que tu código es confiable y tengas solo "dos pequeños hilos" donde "todo es obvio y sencillo", el estado de carrera puede esconderse en la parte más inocua de la lógica.

¿Qué es exactamente un race condition y por qué da tanto miedo? Imagina que dos personas intentan editar al mismo tiempo el mismo papel — uno escribe, el otro borra. A veces todo está bien, otras veces el resultado es ilegible. En programación las consecuencias pueden ser incluso más divertidas: los errores no siempre se manifiestan, solo en condiciones concretas, casi aleatorias.

Race condition (estado de carrera) — situación en la que el resultado de la ejecución del programa depende de qué hilo accedió primero a un recurso o ejecutó una acción. Este problema surge únicamente con acceso concurrente (multihilo), cuando dos o más hilos acceden a datos o recursos compartidos.

¿Qué pasa durante una carrera?

Aquí hay un esquema simple. Imagina que tenemos dos hilos y un recurso compartido (por ejemplo, la variable X):


     +---------+           +---------+          
     | Hilo 1  |           | Hilo 2  |          
     +----+----+           +----+----+          
          |                     |              
          |     Leer X          |              
          | <-------------------|              
          |                     |              
          |     Incrementar X   |              
          |-------------------> |              
          |                     |              
          |     Escribir X      |              
          | <-------------------|              

Si ambos hilos leen simultáneamente el valor de X, lo incrementan y lo escriben de vuelta, uno puede "sobrescribir" los cambios del otro y el número total de incrementos no coincidirá con lo esperado.

2. Ejemplo clásico de Race Condition

Veamos un ejemplo. Supongamos que queremos contar la cantidad de pulsaciones de un botón desde diferentes hilos o la cantidad de tareas procesadas.

Tomamos una variable simple y varios hilos que la incrementan:

using System;
using System.Threading;

class Program
{
    static int counter = 0; // Recurso compartido

    static void Main()
    {
        Thread t1 = new Thread(IncrementCounter);
        Thread t2 = new Thread(IncrementCounter);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine("Valor esperado: 200000");
        Console.WriteLine("Valor real: " + counter);
    }

    static void IncrementCounter()
    {
        for (int i = 0; i < 100_000; i++)
        {
            counter++; // << ¡Aquí puede surgir el problema!
        }
    }
}

¿Qué esperamos?

Puesto que cada hilo incrementa counter 100000 veces, esperamos que el valor final sea 200000.

¿Qué obtenemos en realidad?

A veces — sí, 200000. Pero más a menudo el valor será menor — a veces mucho menor. Repite el experimento y el resultado vacilará.

¿Por qué?

La operación counter++ no es atómica. En realidad se ejecuta así (simplificado):

  1. Leer el valor actual de counter (por ejemplo, 0)
  2. Incrementar en 1 (da 1)
  3. Escribir de vuelta (counter = 1)

Si dos hilos leen el valor antiguo simultáneamente, ambos pueden escribir el mismo valor nuevo, y en efecto solo se habrá sumado un incremento.

Visualización con dos hilos:

Supongamos counter = 0.

  • Hilo 1: lee 0
  • Hilo 2: lee 0
  • Hilo 1: calcula 0 + 1 = 1
  • Hilo 2: calcula 0 + 1 = 1
  • Hilo 1: escribe 1
  • Hilo 2: escribe 1 (pierde el incremento del Hilo 1)

"Felicidades", acabas de perder un incremento. A escala de miles o millones de operaciones — el resultado fluctúa mucho.

3. Más ejemplos: no solo incrementos

Alboroto en la cocina

Para hacerlo más gráfico, imagina un pequeño café. Dos cocineros hacen tortillas en la misma sartén sin coordinarse:

  • El primero pone una tortilla, el segundo inmediatamente pone la suya encima — se mezclan;
  • Uno piensa "ya puse dos tortillas", el otro piensa lo mismo, pero en la sartén hay tres cuando esperaban cuatro;
  • Empieza el caos...

En programación el race condition lleva al mismo "caos": el resultado depende de una secuencia rápida y descontrolada de operaciones.

Cuando los hilos se estorban: acceso simultáneo a datos

Supón que implementas una app bancaria y un cliente deposita y retira dinero del mismo cuenta usando dos hilos (por ejemplo, uno es una transferencia online, otro es caja):

account.Balance += 500;    // Hilo 1: depósito
account.Balance -= 300;    // Hilo 2: retirada

Si estas operaciones no están protegidas, el saldo final puede ser incorrecto: parte de las operaciones simplemente "se pierden" si los hilos actúan simultáneamente.

4. Matices útiles

¿Por qué el race condition es un problema?

Difícil de detectar y reproducir. El bug puede aparecer solo en una máquina con mucha carga o en condiciones raras.

Difícil de depurar. Durante la depuración los hilos pueden "comportarse" diferente y el error desaparecer.

Ruptura de integridad de datos. Obtienes datos incorrectos o corruptos, a veces sin darte cuenta.

Seguridad. En aplicaciones críticas los race conditions pueden llevar a fugas, corrupción de datos e incluso vulnerabilidades.

Diagrama de "timings de la carrera"


+-----------------------+     +-----------------------+
| Hilo 1                |     | Hilo 2                |
+-----------------------+     +-----------------------+
| 1. Leer counter       |     |                       |
| 2. Incrementar counter|     |                       |
| (pero no escribir)    |     |                       |
|                       |     | 1. Leer counter       |
|                       |     | 2. Incrementar counter|
|                       |     | 3. Escribir counter   |
|                       |     | (counter = 1)         |
| 3. Escribir counter   |     |                       |
| (counter = 1)         |     |                       |
+-----------------------+     +-----------------------+

Ambos hilos hicieron el incremento, ¡pero solo se registró uno!

¿Dónde aparece frecuentemente un estado de carrera?

  • Cualquier variable global o estática a la que acceden varios hilos.
  • Listas, colas, colecciones que se llenan desde distintos hilos.
  • Eventos y delegates, si la suscripción/desuscripción ocurre simultáneamente (por ejemplo, UI + tareas background).
  • Cache, diccionarios, gestión de conexiones.
  • Cualquier interacción con archivos, logs, bases de datos sin transacciones o locks.

Cómo evitar race condition: una pequeña introducción

  • ¡Sincronización! (más detalles en próximas lecciones).
  • Usa construcciones del lenguaje y librerías: lock, Monitor, mutexes, semáforos, etc.
  • Para operaciones simples — métodos atómicos (Interlocked.Increment y otros).
  • Usa colecciones thread-safe (ConcurrentBag, ConcurrentDictionary).
  • Siempre pregúntate: "¿qué pasará si mis dos funciones se llaman al mismo tiempo?"

5. Consejos útiles

Consejos para encontrar y diagnosticar carreras

  • No confíes ni en las operaciones más simples (incremento ++, asignación) si usas múltiples hilos.
  • Evita el acceso compartido a variables cuando sea posible.
  • Si ves bugs "flotantes", errores difíciles de reproducir — piensa en carreras.
  • Usa herramientas de análisis de hilos (dotTrace, Concurrency Visualizer, Thread Sanitizer).
  • Realiza tests de carga — cuantas más operaciones y hilos, mayor probabilidad de encontrar el error.

Qué está permitido y qué no sin sincronización

Operación ¿Segura en multihilo? Explicación
Asignación int 🟩 A veces* Sólo si un hilo escribe y los demás solo leen, si no — carrera
Incremento (++/--) 🟥 No ¡No es atómico! Race Condition
Lectura string 🟩 A veces* Si la string no se modifica después de creada
Asignación de objeto 🟩 A veces* Siempre que no haya escrituras concurrentes
Añadir a List<T> 🟥 No List<T> no es thread-safe
Interlocked.Increment
🟩 Sí Método atómico especial

— "A veces" significa que si solo un hilo escribe y todos los demás sólo leen, es seguro; si varios hilos pueden escribir al mismo tiempo — siempre hay carrera.

6. Errores típicos y trampas

En el ejemplo demo arriba vimos counter++ como problema. Otra trampa es incrementar o comprobar un valor dentro de una condición.

Ejemplo: Error divertido con "primera ejecución"

if (!alreadyStarted)
{
    alreadyStarted = true;
    // Hacemos la inicialización...
}

Si esta condición la ejecutan varios hilos al mismo tiempo, cada uno puede ver alreadyStarted == false y entrar. Resultado — inicializan algo dos veces, lo que puede provocar fallos.

1
Cuestionario/control
Introducción a la multihilo, nivel 55, lección 4
No disponible
Introducción a la multihilo
Fundamentos de la multihilo en C#
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION