Solo trato de comprender los subprocesos y la condición de carrera y cómo afectan la salida esperada. En el siguiente código, una vez tuve una salida que comenzaba con "2 Thread-1" y luego "1 Thread-0" .... ¿Cómo podría suceder tal salida? Lo que entiendo es lo siguiente:

Paso 1: asumiendo que el hilo 0 comenzó, incrementó el contador a 1,

Paso 2: antes de imprimirlo, el hilo 1 lo incrementó a 2 y lo imprimió,

Paso 3: El hilo 0 imprime el contador que debería ser 2 pero imprime 1.

¿Cómo podría el hilo 0 imprimir el contador como 1 cuando el hilo 1 ya lo incrementó a 2?

PD: Sé que la clave sincronizada podría lidiar con tales condiciones de carrera, pero solo quiero tener algunos conceptos hechos antes.

public class Counter {
    static int count=0;
    public void add(int value) {
            count=count+value;
            System.out.println(count+" "+ Thread.currentThread().getName());        
    }
}
public class CounterThread extends Thread {
    Counter counter;
    public CounterThread(Counter c) {
        counter=c;
    }
    public void run() {
        for(int i=0;i<5;i++) {
            counter.add(1);
        }
    }
}
public class Main {
    public static void main(String args[]) {
        Counter counter= new Counter();
        Thread t1= new CounterThread(counter);
        Thread t2= new CounterThread(counter);
        t1.start();
        t2.start();
    }
}
1
Hussein Jaber 23 oct. 2019 a las 18:29

1 respuesta

La mejor respuesta

¿Cómo podría el hilo 0 imprimir el contador como 1 cuando el hilo 1 ya lo incrementó a 2?

Hay mucho más sucediendo en estas dos líneas de lo que parece:

count=count+value;
System.out.println(count+" "+ Thread.currentThread().getName());

En primer lugar, el compilador no sabe nada sobre subprocesos. Su trabajo es emitir código que logrará el mismo resultado final cuando se ejecute en un solo hilo. Es decir, cuando todo está dicho y hecho, la cuenta debe incrementarse y el mensaje debe imprimirse.

El compilador tiene mucha libertad para reordenar las operaciones y almacenar valores en registros temporales a fin de garantizar que se logre el resultado final correcto de la manera más eficiente posible. Entonces, por ejemplo, count en la expresión count+" "+... no necesariamente hará que el compilador obtenga el último valor de la variable count global. De hecho, probablemente no buscará de la variable global porque sabe que el resultado de la operación + todavía está en un registro de la CPU. Y, dado que no reconoce que podrían existir otros subprocesos, entonces sabe que no hay forma de que el valor en el registro sea diferente de lo que almacenó en la variable global después de hacer el { {X4}}.


En segundo lugar, al hardware en sí se le permite almacenar valores en lugares temporales y reordenar las operaciones para mejorar la eficiencia, y también se le permite asumir que no hay otros hilos. Por lo tanto, incluso cuando el compilador emite código que dice buscar o almacenar en la variable global en lugar de hacerlo en un registro, el hardware no necesariamente almacena o recupera la dirección real en la memoria.


Suponiendo que su ejemplo de código es código Java, entonces todo eso cambia cuando hace un uso apropiado de los bloques synchronized. Si agrega synchronized a la declaración de su método add, por ejemplo:

public synchronized void add(int value) {
    count=count+value;
    System.out.println(count+" "+ Thread.currentThread().getName());        
}

Eso obliga al compilador a reconocer la existencia de otros subprocesos, y el compilador emitirá instrucciones que obligarán al hardware a reconocer otros subprocesos también.

Al agregar synchronized al método add, fuerza al hardware a entregar el valor real de la variable global al ingresar al método, lo fuerza a escribir realmente el global para cuando el método regrese, y evita que haya más de un hilo en el método al mismo tiempo.

1
Solomon Slow 23 oct. 2019 a las 15:50