A3.C Compartir datos con seguridad¶
El mayor peligro de ejecutar cosas a la vez no es que falle algo de forma evidente, sino que a veces salga mal y a veces no. Son los fallos más difíciles de encontrar.
La condición de carrera¶
En Dart el problema casi no existe: los isolates no comparten memoria, así que no hay una variable que dos hilos puedan pisarse. Si necesitas que varios trabajen sobre lo mismo, cada uno calcula su parte y devuelve un mensaje con el resultado, y tú lo juntas. Es lo que hace el ejemplo: cuatro isolates cuentan hasta 1000 y se suman. (Dentro de un solo isolate el código es secuencial, así que entre un await y otro nadie te interrumpe.)
El ejemplo hace que cuatro isolates sumen 1000 veces cada uno, y repite el experimento tres veces:
import 'dart:isolate';
// En Dart el código se ejecuta en "isolates": cada uno tiene su propia memoria y NO comparten variables.
// Por eso no hay candados: cada isolate calcula lo suyo y devuelve un mensaje con el resultado.
Future<int> contarEnCuatroIsolates() async {
final partes = await Future.wait(List.generate(4, (_) => Isolate.run(() {
var contador = 0; // una variable propia de este isolate
for (var i = 0; i < 1000; i++) {
contador++;
}
return contador; // el valor viaja de vuelta como mensaje
})));
return partes.reduce((a, b) => a + b);
}
Future<void> main() async {
final resultados = <int>[];
for (var i = 0; i < 3; i++) {
resultados.add(await contarEnCuatroIsolates());
}
print('resultados de 3 ejecuciones: ${resultados.join(" ")}');
}
Salida:
resultados de 3 ejecuciones: 4000 4000 4000
El resultado es siempre 4000. Sin protección, ejecutarlo varias veces daría valores distintos y casi siempre menores, porque se pierden sumas: lo peligroso es que a veces coincidiría con 4000 por casualidad y el fallo pasaría desapercibido.
Probar no demuestra que esté bien
Un programa concurrente que funcionó diez veces puede fallar la undécima. Razona sobre qué datos se comparten y quién los modifica, en vez de fiarte de las pruebas.
Mejor que compartir: pasarse mensajes¶
Si dos tareas necesitan intercambiar datos, a menudo es más simple y seguro que no compartan nada y se manden mensajes por un canal o cola. Un productor genera datos, un consumidor los procesa.
Un Stream es una secuencia de valores que llegan con el tiempo. Una función async* los produce con yield y await for los consume, de uno en uno. El productor y el consumidor no comparten variables: solo se pasan valores.
// Productor y consumidor: en lugar de compartir una variable, uno envía mensajes y el otro los recibe.
// En Dart el canal natural es un Stream: «async*» produce valores con yield y «await for» los consume.
Stream<int> producir() async* {
for (var i = 1; i <= 3; i++) {
await Future.delayed(const Duration(milliseconds: 50)); // tarda en tener el dato
print('produce $i');
yield i; // lo entrega a quien esté escuchando
}
}
Future<void> main() async {
var suma = 0;
await for (final n in producir()) {
print('consume $n');
suma += n;
}
print('suma: $suma');
}
Salida:
produce 1
consume 1
produce 2
consume 2
produce 3
consume 3
suma: 6
Mira cómo se intercalan produce y consume: cada dato se consume en cuanto llega, sin esperar a que se produzca el siguiente.
Reglas para dormir tranquilo¶
| Regla | Por qué |
|---|---|
| No compartas si puedes evitarlo: pasa mensajes o devuelve resultados | Sin memoria compartida no hay carreras |
| Si compartes, que sea inmutable (unidad A1.C) | Lo que no cambia, no se pisa |
| Protege todo el acceso al dato compartido, lecturas incluidas | Un solo acceso sin candado reabre el problema |
| Mantén el candado el menor tiempo posible | Un candado largo convierte lo concurrente en secuencial |
| Si usas varios candados, tómalos siempre en el mismo orden | Si no, dos tareas pueden esperarse la una a la otra para siempre (interbloqueo o deadlock) |
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
| Proteger la escritura pero no la lectura | Protege ambas con el mismo candado |
| Dar por válido un resultado porque «salió bien al probar» | Razona sobre qué se comparte, no solo pruebes |
| Olvidar avisar al consumidor de que ya no hay más datos | Usa un valor de fin, o cierra el canal |
| Mantener un candado mientras haces una espera larga | Calcula fuera y entra al candado solo para actualizar el dato |
Para practicar¶
Los ejercicios A3.5 y A3.6 usan un contador protegido y un productor con consumidor. Para ver la misma idea en los otros lenguajes: Pasar de uno a otro.