¿Qué es una condición de carrera?

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

Mis preguntas a la comunidad son:

¿Qué es una condición de carrera? ¿Cómo los detecta? ¿Cómo los manejas? Finalmente, ¿cómo se evita que ocurran?

Una condición de carrera ocurre cuando dos o más hilos pueden acceder a los datos compartidos e intentan cambiarlos al mismo tiempo. Debido a que el algoritmo de progtwigción de subprocesos puede intercambiar entre subprocesos en cualquier momento, no se 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 progtwigción de hilos, es decir, ambos hilos están “acelerando” para acceder / cambiar los datos.

Los problemas a menudo ocurren cuando un hilo hace un “verificar y actuar” (por ejemplo, “verificar” si el valor es X, luego “actuar” para hacer algo que depende de que el valor sea X) y otro hilo hace algo con el valor de 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 el cheque y el acto. No tienes forma real de saber.

Para evitar que se produzcan condiciones de carrera, normalmente pondría un candado alrededor de 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 

Una “condición de carrera” existe cuando el código multiproceso (o 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 tenía 5 hilos ejecutando este código a la vez, el valor de x NO terminaría en 50,000,000. De hecho, variaría con cada carrera.

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
 Agrega 1 a este valor
 Almacene este valor en x

Cualquier hilo puede estar en cualquier paso de este proceso en cualquier momento, y pueden pisar uno al otro cuando se trata de un recurso compartido. El estado de x puede cambiarse por otro hilo durante el tiempo entre x se lee y cuando se escribe de nuevo.

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 ambos almacenarán el mismo valor (x + 1) en x!

Ejemplo:

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

Las condiciones de carrera se pueden evitar empleando algún tipo de mecanismo de locking 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 locking, busque: mutex, semáforo, sección crítica, recurso compartido.

¿Qué es una condición de carrera?

Estás planeando ir al cine a las 5 p.m. Usted pregunta por la disponibilidad de los boletos a las 4 p.m. El representante dice que están disponibles. Te relajas y alcanzas 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 el cheque y la acción. Inquirió a las 4 y actuó a las 5. Mientras tanto, alguien más agarró las entradas. Esa es una condición de carrera, específicamente un escenario de “verificar y actuar” de condiciones de carrera.

¿Cómo los detecta?

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

¿Cómo manejas y previenes?

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

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

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.

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

Una condición de carrera es un error semántico. Es un defecto que ocurre en el tiempo o en el orden de los eventos que lleva a un comportamiento erróneo del progtwig.

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 ni necesarias ni suficientes para el otro. Esta publicación de blog también explica la diferencia muy bien, 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 existe una forma general de detectarlos. Esto se debe a que no hay forma de tener un oracle automático que pueda distinguir entre el comportamiento correcto y el incorrecto del progtwig en el caso general. La detección de carreras es un problema indecidible.

Por otro lado, las carreras de datos tienen una definición precisa que no se relaciona necesariamente con la corrección y, por lo tanto, se pueden detectar. Hay muchos sabores de detectores de raza de datos (detección de carrera de datos estáticos / dynamics, detección de carrera de datos basada en cerrojo, detección de carrera de datos basada en lockset anterior, detección de raza híbrida de datos). Un detector de carreras de datos dynamics 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 progtwigción para inducir a que ocurra, antes de los límites entre los accesos a los datos compartidos (durante el desarrollo o una vez que se detectan usando las herramientas mencionadas anteriormente). esto se puede hacer a través de lockings, variables de condición, semáforos, etc. Sin embargo, también se pueden emplear diferentes paradigmas de progtwigción, como pasar mensajes (en lugar de memoria compartida), que evitan carreras de datos por construcción.

Una definición de tipo de 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 anterior 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 en general debería evitarse. @Steve Gury da un buen ejemplo de cuándo podría ser un problema.

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 el hilo A se adelanta justo después de haber verificado que el objeto.a no es nulo, B hará a = 0 , y cuando el hilo A obtendrá el procesador, hará una “división por cero”.

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

Las condiciones de carrera ocurren en aplicaciones de subprocesos múltiples o sistemas multiproceso. Una condición de carrera, en su aspecto más básico, es cualquier cosa que suponga que dos cosas que no están en el mismo proceso o subproceso ocurrirán en un orden particular, sin tomar medidas para garantizar que lo hagan. Esto sucede comúnmente cuando dos hilos pasan mensajes estableciendo y verificando las variables miembro de una clase a la que ambos pueden acceder. Casi siempre hay una condición de carrera cuando un hilo se pone en suspensión para dar otro tiempo de hilo para finalizar una tarea (a menos que el sueño esté en un bucle, con algún mecanismo de comprobación).

Las herramientas para prevenir las condiciones de carrera dependen del lenguaje y del sistema operativo, pero algunos comon son mutex, secciones críticas y señales. Los mutexes son buenos cuando quieres asegurarte de que eres el único que está haciendo 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 depende en gran medida de las horas de sueño es propenso a las condiciones de carrera, por lo que primero verifique que las llamadas duerman en el código afectado. También se puede utilizar la adición de long sleeps para la depuración para intentar forzar un orden particular de eventos. Esto puede ser útil para reproducir el comportamiento, ver si puede hacerlo desaparecer cambiando el momento de las cosas y para poner a prueba las soluciones de prueba. Los sleeps deben eliminarse después de la depuración.

Sin embargo, el signo distintivo de que uno tiene una condición de carrera es si hay un problema que solo ocurre intermitentemente en algunas máquinas. Los errores comunes serían lockings y lockings. Con el registro, debería poder encontrar el área afectada y trabajar desde allí.

La condición de carrera no solo está relacionada con el software, sino que también está relacionada 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 en la salida primero .

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

enter image description here

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

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

  • “dos señales” => “dos hilos” / “dos procesos”
  • “influir en el resultado” => “influir en algún estado compartido”

Entonces, 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 alguna diferencia de tiempo sutil, que podría ser causada por algún orden de lanzamiento de hilo / proceso, progtwigción de hilo / proceso, etc.

Microsoft realmente ha publicado un artículo realmente detallado sobre este tema de condiciones de carrera y puntos muertos. El resumen más resumido de ello 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 en el valor y corren para ver qué subproceso puede escribir el último valor en la variable compartida. El valor del subproceso que escribe su valor al último se conserva, porque el subproceso está escribiendo sobre el valor que escribió el subproceso anterior.

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

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 poder hecho correctamente

En la memoria o el almacenamiento de la computadora, puede producirse una condición de carrera si se reciben comandos para leer y escribir una gran cantidad de datos casi en el mismo instante, y la máquina intenta sobrescribir algunos o todos los datos antiguos mientras se siguen utilizando los datos antiguos. leer. El resultado puede ser uno o más de los siguientes: un locking de la computadora, una notificación de “operación ilegal” y el cierre del progtwig, errores al leer los datos antiguos o errores al escribir los datos nuevos.

Aquí está el clásico ejemplo de saldo de cuenta bancaria que ayudará a los novatos a entender los hilos en Java fácilmente con las 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)); } 

Ok eso son 4 preguntas. una por una respuesta es como debajo …

¿Qué es una condición de carrera?

Ocurre cuando la salida y / o el resultado del proceso es críticamente dependiente de la secuencia o el tiempo de otros eventos, es decir, 2 señales están corriendo para cambiar primero la salida.

¿Cómo los detecta?

Esto conduce a un error que es difícil de localizar.

¿Cómo los manejas?

Usa semáforos

Y finalmente,

¿Cómo se evita que ocurran?

Una forma de evitar la condición de carrera es usar un mecanismo de locking para los recursos. pero el locking de recursos puede llevar a lockings. que tiene que ser tratado.

Pruebe este ejemplo básico para una mejor comprensión de la condición de 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; } } } 

No siempre deseas descartar una condición de carrera. Si tiene una bandera que puede ser leída y escrita por múltiples hilos, y esta bandera está configurada como ‘hecha’ por una cadena para que el proceso de detención de otra cadena cuando ‘flag’ esté configurado como ‘hecho’, no desee esa “carrera” condición “para ser eliminado. De hecho, este puede ser referido como una condición de raza benigna.

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

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

Considere una operación que debe mostrar el recuento tan pronto como se incremente el recuento. 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 locking 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 utilizando Synchronzation

Puede evitar condiciones de carrera , si usa clases “Atómicas”. La razón es solo 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 hiciste 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!