¿Qué es una condición de carrera?


982

Al escribir aplicaciones multiproceso, uno de los problemas más comunes experimentados son las condiciones de carrera.

Mis preguntas a la comunidad son:

¿Cuál es la condición de carrera?
¿Cómo los detectas?
¿Cómo los manejas?
Finalmente, ¿cómo evita que ocurran?


3
Hay un gran capítulo en el COMO de Programación segura para Linux que describe qué son y cómo evitarlos.
Craig H

44
Me gustaría mencionar que, sin especificar el idioma, la mayoría de las partes de esta pregunta no se pueden responder correctamente, porque en diferentes idiomas, la definición, las consecuencias y las herramientas para prevenirlas pueden diferir.
MikeMB

@MikeMB. De acuerdo, excepto cuando se analiza la ejecución del código de bytes, como lo hace Race Catcher (consulte este hilo stackoverflow.com/a/29361427/1363844 ) podemos abordar todos esos aproximadamente 62 idiomas que se compilan para el código de bytes (ver en.wikipedia.org / wiki / List_of_JVM_languages )
Ben

Respuestas:


1238

Una condición de carrera ocurre cuando dos o más subprocesos pueden acceder a datos compartidos e intentan cambiarlos al mismo tiempo. Debido a que el algoritmo de programación de subprocesos puede intercambiar entre subprocesos en cualquier momento, no conoce el orden en que los subprocesos intentarán acceder a los datos compartidos. Por lo tanto, el resultado del cambio en los datos depende del algoritmo de programación de subprocesos, es decir, ambos subprocesos "compiten" para acceder / cambiar los datos.

Los problemas a menudo ocurren cuando un hilo hace un "chequeo-luego-acto" (por ejemplo, "cheque" si el valor es X, luego "actúa" para hacer algo que depende de que el valor sea X) y otro hilo hace algo al valor en entre el "cheque" y el "acto". P.ej:

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"

   // If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
   // y will not be equal to 10.
}

El punto es que y podría ser 10, o podría ser cualquier cosa, dependiendo de si otro hilo cambió x entre la verificación y el acto. No tienes forma real de saberlo.

Para evitar que se produzcan condiciones de carrera, normalmente se debe bloquear los datos compartidos para garantizar que solo un hilo pueda acceder a los datos a la vez. Esto significaría algo como esto:

// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released. 
              // Therefore y = 10
}
// release lock for x

121
¿Qué hace el otro hilo cuando encuentra el bloqueo? ¿Espera? ¿Error?
Brian Ortiz el

174
Sí, el otro hilo tendrá que esperar hasta que se libere el bloqueo para poder continuar. Esto hace que sea muy importante que el hilo de retención libere el bloqueo cuando haya terminado con él. Si nunca lo libera, entonces el otro hilo esperará indefinidamente.
Lehane

2
@Ian En un sistema multiproceso, siempre habrá momentos en que los recursos deben compartirse. Decir que un enfoque es malo sin dar una alternativa simplemente no es productivo. Siempre estoy buscando formas de mejorar y, si hay una alternativa, con gusto la investigaré y sopesaré los pros y los contras.
Despertar

2
@Despertar ... además, no es necesariamente el caso de que los recursos siempre tengan que compartirse en un sistema de subprocesos múltiples. Por ejemplo, puede tener una matriz donde cada elemento necesita procesamiento. Posiblemente podría particionar la matriz y tener un subproceso para cada partición y los subprocesos pueden hacer su trabajo de forma completamente independiente uno del otro.
Ian Warburton

12
Para que se produzca una carrera, es suficiente que un solo hilo intente cambiar los datos compartidos mientras que el resto de los hilos pueden leerlo o cambiarlo.
SomeWittyUsername

213

Existe una "condición de carrera" cuando el código multiproceso (o de otro modo paralelo) que accedería a un recurso compartido podría hacerlo de tal manera que cause resultados inesperados.

Toma este ejemplo:

for ( int i = 0; i < 10000000; i++ )
{
   x = x + 1; 
}

Si tuvieras 5 hilos ejecutando este código a la vez, el valor de x NO SERÍA 50,000,000. De hecho, variaría con cada ejecución.

Esto se debe a que, para que cada subproceso incremente el valor de x, tienen que hacer lo siguiente: (simplificado, obviamente)

Recuperar el valor de x
Agregue 1 a este valor
Almacene este valor en x

Cualquier hilo puede estar en cualquier paso de este proceso en cualquier momento, y pueden pisarse mutuamente cuando se trata de un recurso compartido. El estado de x puede ser cambiado por otro hilo durante el tiempo entre x se lee y cuando se vuelve a escribir.

Digamos que un hilo recupera el valor de x, pero aún no lo ha almacenado. Otro hilo también puede recuperar el mismo valor de x (porque ningún hilo lo ha cambiado todavía) y luego ambos estarían almacenando el mismo valor (x + 1) en x!

Ejemplo:

Hilo 1: lee x, el valor es 7
Hilo 1: agregue 1 a x, el valor ahora es 8
Hilo 2: lee x, el valor es 7
Hilo 1: almacena 8 en x
Hilo 2: agrega 1 a x, el valor ahora es 8
Hilo 2: almacena 8 en x

Las condiciones de carrera se pueden evitar empleando algún tipo de mecanismo de bloqueo antes del código que accede al recurso compartido:

for ( int i = 0; i < 10000000; i++ )
{
   //lock x
   x = x + 1; 
   //unlock x
}

Aquí, la respuesta sale como 50,000,000 cada vez.

Para obtener más información sobre el bloqueo, busque: mutex, semáforo, sección crítica, recurso compartido.


Vea jakob.engbloms.se/archives/65 para ver un ejemplo de un programa para probar qué tan a menudo van mal esas cosas ... realmente depende del modelo de memoria de la máquina en la que se está ejecutando.
jakobengblom2

1
¿Cómo puede llegar a 50 millones si tiene que detenerse en 10 millones?

99
@nocomprende: por 5 hilos ejecutando el mismo código a la vez, como se describe directamente debajo del fragmento ...
Jon Skeet

44
@ JonSkeet Tienes razón, confundí la i y la x. Gracias.

El bloqueo de doble verificación al implementar el patrón Singleton es un ejemplo de prevención de la condición de carrera.
Bharat Dodeja

150

¿Qué es una condición de carrera?

Estás planeando ir al cine a las 5 pm. Usted pregunta por la disponibilidad de los boletos a las 4 pm. El representante dice que están disponibles. Te relajas y llegas a la taquilla 5 minutos antes del espectáculo. Estoy seguro de que puedes adivinar lo que sucede: es una casa llena. El problema aquí estaba en la duración entre la verificación y la acción. Usted preguntó a las 4 y actuó a las 5. Mientras tanto, alguien más agarró los boletos. Esa es una condición de carrera, específicamente un escenario de "comprobar y luego actuar" de las condiciones de carrera.

¿Cómo los detectas?

Revisión del código religioso, pruebas unitarias multiproceso. No hay ningún atajo. Hay pocos complementos de Eclipse emergentes en esto, pero nada estable todavía.

¿Cómo los maneja y previene?

Lo mejor sería crear funciones libres de efectos secundarios y sin estado, usar inmutables tanto como sea posible. Pero eso no siempre es posible. Por lo tanto, usar java.util.concurrent.atomic, las estructuras de datos concurrentes, la sincronización adecuada y la concurrencia basada en actores ayudarán.

El mejor recurso para la concurrencia es JCIP. También puede obtener más detalles sobre la explicación anterior aquí .


Las revisiones de código y las pruebas unitarias son secundarias para modelar el flujo entre los oídos y hacer menos uso de la memoria compartida.
Acumenus

2
Aprecié el ejemplo del mundo real de una condición de carrera
Tom O.

11
Me gusta la respuesta pulgares arriba . La solución es: bloquea los tickets entre 4-5 con mutex (excepción mutua, c ++). En el mundo real se llama reserva de entradas :)
Volt

1
sería una respuesta decente si dejara caer los bits solo de Java (la pregunta no es sobre Java, sino más bien las condiciones de carrera en general)
Corey Goldberg

No. Esto no es una condición de carrera. Desde la perspectiva del "negocio", simplemente esperó demasiado. Obviamente, el pedido pendiente no es una solución. Pruebe un scalper, de lo contrario solo compre el boleto como seguro
csherriff

65

Hay una diferencia técnica importante entre las condiciones de carrera y las carreras de datos. La mayoría de las respuestas parecen suponer que estos términos son equivalentes, pero no lo son.

Una carrera de datos ocurre cuando 2 instrucciones acceden a la misma ubicación de memoria, al menos uno de estos accesos es una escritura y no sucede antes de ordenar entre estos accesos. Ahora, lo que constituye un suceso antes de ordenar está sujeto a mucho debate, pero en general los pares ulock-lock en la misma variable de bloqueo y los pares de señal de espera en la misma variable de condición inducen un orden de suceso anterior.

Una condición de carrera es un error semántico. Es una falla que ocurre en el momento o en el orden de los eventos que conduce a un comportamiento erróneo del programa .

Muchas condiciones de carrera pueden ser (y de hecho son) causadas por carreras de datos, pero esto no es necesario. De hecho, las carreras de datos y las condiciones de carrera no son la condición necesaria ni suficiente para el otro. Esta publicación de blog también explica muy bien la diferencia, con un simple ejemplo de transacción bancaria. Aquí hay otro ejemplo simple que explica la diferencia.

Ahora que definimos la terminología, intentemos responder la pregunta original.

Dado que las condiciones de carrera son errores semánticos, no hay una forma general de detectarlos. Esto se debe a que no hay forma de tener un oráculo automatizado que pueda distinguir el comportamiento del programa correcto frente al incorrecto en el caso general. La detección de la raza es un problema indecidible.

Por otro lado, las carreras de datos tienen una definición precisa que no necesariamente se relaciona con la corrección y, por lo tanto, uno puede detectarlas. Hay muchos tipos de detectores de carrera de datos (detección de carrera de datos estática / dinámica, detección de carrera de datos basada en bloqueos, detección de carrera de datos basada en eventos anteriores, detección de carrera de datos híbrida). Un detector de carrera de datos dinámico de última generación es ThreadSanitizer, que funciona muy bien en la práctica.

El manejo de las carreras de datos en general requiere cierta disciplina de programación para inducir bordes antes de los accesos a los datos compartidos (ya sea durante el desarrollo o una vez que se detectan utilizando las herramientas mencionadas anteriormente). Esto se puede hacer a través de bloqueos, variables de condición, semáforos, etc. Sin embargo, también se pueden emplear diferentes paradigmas de programación como el paso de mensajes (en lugar de la memoria compartida) que evitan las carreras de datos por construcción.


La diferencia es crítica para entender la condición de la carrera. ¡Gracias!
ProgramCpp

37

Una definición de tipo canónico es " cuando dos hilos acceden a la misma ubicación en la memoria al mismo tiempo, y al menos uno de los accesos es una escritura ". En la situación, el hilo "lector" puede obtener el valor antiguo o el nuevo valor, dependiendo de qué hilo "gana la carrera". Esto no siempre es un error, de hecho, algunos algoritmos de bajo nivel realmente peludos lo hacen a propósito, pero generalmente se debe evitar. @Steve Gury es un buen ejemplo de cuándo podría ser un problema.


3
¿Podría dar un ejemplo de cómo las condiciones de carrera pueden ser útiles? Buscar en Google no ayudó.
Alex V.

3
@Alex V. En este punto, no tengo idea de lo que estaba hablando. Creo que esto puede haber sido una referencia a la programación sin bloqueo, pero no es realmente exacto decir que depende de las condiciones de carrera, per se.
Chris Conway

33

Una condición de carrera es un tipo de error, que ocurre solo con ciertas condiciones temporales.

Ejemplo: imagina que tienes dos hilos, A y B.

En el hilo A:

if( object.a != 0 )
    object.avg = total / object.a

En el hilo B:

object.a = 0

Si se adelanta el subproceso A justo después de comprobar ese objeto. A no es nulo, B lo hará a = 0, y cuando el subproceso A gane el procesador, hará una "división por cero".

Este error solo ocurre cuando el hilo A se adelanta justo después de la declaración if, es muy raro, pero puede suceder.


21

La condición de carrera no solo está relacionada con el software sino también con el hardware. En realidad, el término fue inicialmente acuñado por la industria del hardware.

De acuerdo con wikipedia :

El término se origina con la idea de dos señales compitiendo entre sí para influir primero en la salida .

Condición de carrera en un circuito lógico:

ingrese la descripción de la imagen aquí

La industria del software tomó este término sin modificación, lo que hace que sea un poco difícil de entender.

Debe hacer algún reemplazo para asignarlo al mundo del software:

  • "dos señales" => "dos hilos" / "dos procesos"
  • "influir en la salida" => "influir en algún estado compartido"

Por lo tanto, la condición de carrera en la industria del software significa "dos hilos" / "dos procesos" compitiendo entre sí para "influir en un estado compartido", y el resultado final del estado compartido dependerá de una sutil diferencia de tiempo, que podría ser causada por algún orden de lanzamiento de subprocesos / procesos, programación de subprocesos / procesos, etc.


20

Una condición de carrera es una situación en la programación concurrente donde dos subprocesos o procesos concurrentes compiten por un recurso y el estado final resultante depende de quién obtiene el recurso primero.


simplemente brillante explicación
gokareless

Estado final de qué?
Roman Alexandrovich

1
@RomanAlexandrovich El estado final del programa. El estado que se refiere a cosas como los valores de las variables, etc. Ver la excelente respuesta de Lehane. El "estado" en su ejemplo se referiría a los valores finales de 'x' e 'y'.
AMTerp

19

Las condiciones de carrera ocurren en aplicaciones multiproceso o sistemas multiproceso. Una condición de carrera, en su forma más básica, es cualquier cosa que asume que dos cosas que no están en el mismo hilo o proceso sucederán en un orden particular, sin tomar medidas para garantizar que así sea. Esto sucede comúnmente cuando dos hilos pasan mensajes configurando y verificando las variables miembro de una clase a la que ambos pueden acceder. Casi siempre hay una condición de carrera cuando un subproceso llama a suspensión para dar tiempo a otro subproceso para finalizar una tarea (a menos que esa suspensión esté en un bucle, con algún mecanismo de verificación).

Las herramientas para prevenir las condiciones de carrera dependen del idioma y del sistema operativo, pero algunas más comunes son mutexes, secciones críticas y señales. Los mutexes son buenos cuando quieres asegurarte de que eres el único que hace algo. Las señales son buenas cuando quieres asegurarte de que alguien más haya terminado de hacer algo. Minimizar los recursos compartidos también puede ayudar a prevenir comportamientos inesperados

Detectar las condiciones de carrera puede ser difícil, pero hay un par de signos. El código que se basa en gran medida en dormir es propenso a las condiciones de carrera, por lo que primero verifique si hay llamadas para dormir en el código afectado. Agregar sueños particularmente largos también se puede utilizar para la depuración para tratar de forzar un orden particular de eventos. Esto puede ser útil para reproducir el comportamiento, ver si puede hacer que desaparezca cambiando el tiempo de las cosas y para probar las soluciones implementadas. Los durmientes deben eliminarse después de la depuración.

Sin embargo, la señal de firma de que uno tiene una condición de carrera es si hay un problema que solo ocurre de manera intermitente en algunas máquinas. Los errores comunes serían bloqueos y puntos muertos. Con el registro, debería poder encontrar el área afectada y trabajar desde allí.


10

Microsoft ha publicado un artículo realmente detallado sobre este tema de las condiciones de carrera y puntos muertos. El resumen más resumido sería el párrafo del título:

Una condición de carrera ocurre cuando dos hilos acceden a una variable compartida al mismo tiempo. El primer hilo lee la variable, y el segundo hilo lee el mismo valor de la variable. Luego, el primer subproceso y el segundo subproceso realizan sus operaciones sobre el valor, y corren para ver qué subproceso puede escribir el último valor en la variable compartida. El valor del hilo que escribe su último valor se conserva, porque el hilo está escribiendo sobre el valor que escribió el hilo anterior.


5

¿Qué es una condición de carrera?

La situación cuando el proceso depende críticamente de la secuencia o el momento de otros eventos.

Por ejemplo, el procesador A y el procesador B necesitan recursos idénticos para su ejecución.

¿Cómo los detectas?

Hay herramientas para detectar la condición de carrera automáticamente:

¿Cómo los manejas?

La condición de carrera puede ser manejada por Mutex o Semáforos . Actúan como un bloqueo que permite que un proceso adquiera un recurso en función de ciertos requisitos para evitar la condición de carrera.

¿Cómo evita que ocurran?

Hay varias formas de prevenir la condición de la carrera, como evitar la sección crítica .

  1. No hay dos procesos simultáneamente dentro de sus regiones críticas. ( Exclusión mutua)
  2. No se hacen suposiciones sobre las velocidades o el número de CPU.
  3. Ningún proceso se ejecuta fuera de su región crítica que bloquea otros procesos.
  4. Ningún proceso tiene que esperar para siempre para ingresar a su región crítica. (A espera recursos B, B espera recursos C, C espera recursos A)

2

Una condición de carrera es una situación indeseable que ocurre cuando un dispositivo o sistema intenta realizar dos o más operaciones al mismo tiempo, pero debido a la naturaleza del dispositivo o sistema, las operaciones deben realizarse en la secuencia adecuada para que hecho correctamente

En la memoria o el almacenamiento de la computadora, puede ocurrir una condición de carrera si se reciben comandos para leer y escribir una gran cantidad de datos en casi el mismo instante, y la máquina intenta sobrescribir algunos o todos los datos antiguos mientras todavía se están usando esos datos antiguos. leer. El resultado puede ser uno o más de los siguientes: un bloqueo de la computadora, una "operación ilegal", notificación y apagado del programa, errores al leer los datos antiguos o errores al escribir los datos nuevos.


2

Aquí está el ejemplo clásico de Saldo de cuenta bancaria que ayudará a los novatos a comprender los hilos en Java fácilmente en condiciones de carrera:

public class BankAccount {

/**
 * @param args
 */
int accountNumber;
double accountBalance;

public synchronized boolean Deposit(double amount){
    double newAccountBalance=0;
    if(amount<=0){
        return false;
    }
    else {
        newAccountBalance = accountBalance+amount;
        accountBalance=newAccountBalance;
        return true;
    }

}
public synchronized boolean Withdraw(double amount){
    double newAccountBalance=0;
    if(amount>accountBalance){
        return false;
    }
    else{
        newAccountBalance = accountBalance-amount;
        accountBalance=newAccountBalance;
        return true;
    }
}

public static void main(String[] args) {
    // TODO Auto-generated method stub
    BankAccount b = new BankAccount();
    b.accountBalance=2000;
    System.out.println(b.Withdraw(3000));

}

1

Puede evitar la condición de carrera , si utiliza clases "atómicas". La razón es solo que el hilo no separa la operación get y set, el ejemplo está a continuación:

AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);

Como resultado, tendrá 7 en el enlace "ai". Aunque realizó dos acciones, pero ambas operaciones confirman el mismo hilo y ningún otro hilo interferirá en esto, ¡eso significa que no hay condiciones de carrera!


0

Pruebe este ejemplo básico para comprender mejor la condición de la carrera:

    public class ThreadRaceCondition {

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Account myAccount = new Account(22222222);

        // Expected deposit: 250
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.DEPOSIT, 5.00);
            t.start();
        }

        // Expected withdrawal: 50
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.WITHDRAW, 1.00);
            t.start();

        }

        // Temporary sleep to ensure all threads are completed. Don't use in
        // realworld :-)
        Thread.sleep(1000);
        // Expected account balance is 200
        System.out.println("Final Account Balance: "
                + myAccount.getAccountBalance());

    }

}

class Transaction extends Thread {

    public static enum TransactionType {
        DEPOSIT(1), WITHDRAW(2);

        private int value;

        private TransactionType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    };

    private TransactionType transactionType;
    private Account account;
    private double amount;

    /*
     * If transactionType == 1, deposit else if transactionType == 2 withdraw
     */
    public Transaction(Account account, TransactionType transactionType,
            double amount) {
        this.transactionType = transactionType;
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        switch (this.transactionType) {
        case DEPOSIT:
            deposit();
            printBalance();
            break;
        case WITHDRAW:
            withdraw();
            printBalance();
            break;
        default:
            System.out.println("NOT A VALID TRANSACTION");
        }
        ;
    }

    public void deposit() {
        this.account.deposit(this.amount);
    }

    public void withdraw() {
        this.account.withdraw(amount);
    }

    public void printBalance() {
        System.out.println(Thread.currentThread().getName()
                + " : TransactionType: " + this.transactionType + ", Amount: "
                + this.amount);
        System.out.println("Account Balance: "
                + this.account.getAccountBalance());
    }
}

class Account {
    private int accountNumber;
    private double accountBalance;

    public int getAccountNumber() {
        return accountNumber;
    }

    public double getAccountBalance() {
        return accountBalance;
    }

    public Account(int accountNumber) {
        this.accountNumber = accountNumber;
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean deposit(double amount) {
        if (amount < 0) {
            return false;
        } else {
            accountBalance = accountBalance + amount;
            return true;
        }
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean withdraw(double amount) {
        if (amount > accountBalance) {
            return false;
        } else {
            accountBalance = accountBalance - amount;
            return true;
        }
    }
}

0

No siempre quieres descartar una condición de carrera. Si tiene un indicador que puede leerse y escribirse en varios subprocesos, y este indicador está configurado como 'hecho' por un subproceso para que otro subproceso deje de procesarse cuando el indicador está configurado como 'hecho', no desea esa "carrera" condición "a ser eliminada. De hecho, esta puede ser referida como una condición de carrera benigna.

Sin embargo, al usar una herramienta para la detección de la condición de carrera, se detectará como una condición de carrera dañina.

Más detalles sobre la condición de la carrera aquí, http://msdn.microsoft.com/en-us/magazine/cc546569.aspx .


¿En qué idioma se basa su respuesta?
MikeMB

Francamente, me parece que si tienes condiciones de carrera per se , no estás diseñando tu código de manera estrictamente controlada. Lo cual, si bien puede no ser un problema en su caso teórico, es evidencia de problemas más grandes con la forma en que diseña y desarrolla software. Espere enfrentar errores dolorosos de condición de carrera tarde o temprano.
Ingeniero

0

Considere una operación que debe mostrar el recuento tan pronto como el recuento se incremente. es decir, tan pronto como CounterThread incremente el valor DisplayThread necesita mostrar el valor actualizado recientemente.

int i = 0;

Salida

CounterThread -> i = 1  
DisplayThread -> i = 1  
CounterThread -> i = 2  
CounterThread -> i = 3  
CounterThread -> i = 4  
DisplayThread -> i = 4

Aquí CounterThread obtiene el bloqueo con frecuencia y actualiza el valor antes de que DisplayThread lo muestre. Aquí existe una condición de carrera. La condición de carrera se puede resolver mediante la sincronización


0

Una condición de carrera es una situación indeseable que ocurre cuando dos o más procesos pueden acceder y cambiar los datos compartidos al mismo tiempo. Ocurrió porque hubo accesos conflictivos a un recurso. El problema crítico de la sección puede causar condición de carrera. Para resolver la condición crítica entre el proceso, hemos eliminado solo un proceso a la vez que ejecuta la sección crítica.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.