java - programacion - ¿Por qué es mala la mutabilidad compartida?
manual de programacion android pdf (3)
El problema entra en juego cuando se realiza un procesamiento paralelo. Hace un tiempo leí un blog de Henrik Eichenhardt respondiendo por qué un estado mutable compartido es la raíz de todo mal.
Este es un breve razonamiento sobre por qué la mutabilidad compartida no es buena; extraído del blog.
no determinismo = procesamiento paralelo + estado mutable
Esta ecuación básicamente significa que tanto el procesamiento paralelo como el estado mutable combinados dan como resultado un comportamiento del programa no determinista . Si solo hace un procesamiento paralelo y tiene un estado inmutable, todo está bien y es fácil razonar sobre los programas. Por otro lado, si desea realizar un procesamiento en paralelo con datos mutables, debe sincronizar el acceso a las variables mutables que esencialmente procesa estas secciones del programa con un solo hilo. Esto no es realmente nuevo, pero no he visto estos conceptos expresados tan elegantemente. Un programa no determinista está roto .
Este blog continúa derivando los detalles internos de por qué los programas paralelos sin la sincronización adecuada están rotos, lo cual se puede encontrar dentro del enlace adjunto.
Estaba viendo una presentación en Java, y en un momento dado, el conferenciante dijo:
"La mutable está bien, compartir es agradable, la mutabilidad compartida es trabajo del diablo".
A lo que se estaba refiriendo es a la siguiente pieza de código, que consideró un "hábito extremadamente malo":
//double the even values and put that into a list.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);
List<Integer> doubleOfEven = new ArrayList<>();
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * 2)
.forEach(e -> doubleOfEven.add(e));
Luego procedió a escribir el código que debería usarse, que es:
List<Integer> doubleOfEven2 =
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * 2)
.collect(toList());
No entiendo por qué la primera pieza del código es "mala costumbre". Para mí, ambos logran el mismo objetivo.
La cuestión es que la conferencia es un poco incorrecta al mismo tiempo. El ejemplo que proporcionó usa forEach
, que está documentado como:
El comportamiento de esta operación es explícitamente no determinista. Para oleoductos paralelos, esta operación no garantiza respetar el orden de encuentro de la secuencia , ya que al hacerlo se sacrificaría el beneficio del paralelismo ...
Podrías usar:
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * 2)
.parallel()
.forEachOrdered(e -> doubleOfEven.add(e));
Y siempre tendrías el mismo resultado garantizado.
Por otro lado, el ejemplo que usa Collectors.toList
es mejor, porque los colectores respetan el encounter order
, por lo que funciona bien.
El punto interesante es que Collectors.toList
usa ArrayList
debajo que no es una colección segura para subprocesos . Es solo que usa muchos de ellos (para procesamiento paralelo) y se fusiona al final.
Una última nota que paralelo y secuencial no influye en el orden de encuentro , es la operación aplicada al Stream
que lo hace. Excelente lectura here .
También debemos pensar que incluso el uso de una colección segura para subprocesos aún no es seguro con Streams por completo, especialmente cuando confías en side-effects
.
List<Integer> numbers = Arrays.asList(1, 3, 3, 5);
Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
List<Integer> collected = numbers.stream()
.parallel()
.map(e -> {
if (seen.add(e)) {
return 0;
} else {
return e;
}
})
.collect(Collectors.toList());
System.out.println(collected);
collected
en este punto podría ser [0,3,0,0]
OR [0,0,3,0]
u otra cosa.
Supongamos que dos hilos realizan esta tarea al mismo tiempo, el segundo hilo una instrucción detrás del primero.
El primer hilo crea doubleOfEven. El segundo hilo crea doubleOfEven, la instancia creada por el primer hilo será basura. Entonces ambos hilos agregarán los dobles de todos los números pares a doubleOfEvent, por lo que contendrá 0, 0, 4, 4, 8, 8, 12, 12, ... en lugar de 0, 4, 8, 12 ... ( En realidad, estos hilos no estarán perfectamente sincronizados, por lo que cualquier cosa que pueda salir mal saldrá mal).
No es que la segunda solución sea mucho mejor. Tendría dos hilos configurando el mismo global. En este caso, lo configuran en valores lógicamente iguales, pero si lo configuran en dos valores diferentes, entonces no sabrá qué valor tiene después. Un hilo no obtendrá el resultado que quiere.