Práctica 1.3: Concurrencia de alto nivel y garantías del lenguaje
Fecha límite de entrega: 20 de octubre de 2026 a las 15:00
Lenguajes de trabajo: Java (JVM ThreadPools & Atomics) y Rust (Fearless Concurrency)
Código de inicio: 📥 game_of_life_java_rust.zip
1. Introducción y objetivos
- Continuar la simulación del Juego de la Vida: Portar la lógica del autómata celular de Conway (introducido en la P1.2) a Java y Rust.
- Gestión de hilos en entornos industriales (Java): Sustituir la creación manual de hilos por gestores de pools de hilos (
ExecutorService) para evitar la sobrecarga de instanciación en la JVM. - Distinguir visibilidad de atomicidad: Comprender el papel de la palabra clave
volatilefrente a los tipos atómicos (AtomicInteger) bajo accesos concurrentes en memoria. - Experimentar la concurrencia garantizada por compilador (Rust): Comprender las reglas del Borrow Checker y cómo el sistema de tipos de Rust prohíbe las condiciones de carrera en tiempo de compilación mediante
Arc<Mutex<T>>.
2. El Juego de la Vida en Java y Rust
En esta práctica continuaremos trabajando sobre el autómata celular del Juego de la Vida (Conway's Game of Life) introducido en la Práctica 1.2.
En lugar de implementar la sincronización manual de bajo nivel en C mediante POSIX Threads, adaptaremos la computación del estado de las celdas () a las construcciones de alto nivel de dos lenguajes modernos de producción:
- En Java: Dividirás el cálculo del tablero entre las tareas de un pool de hilos (
ExecutorService) y mantendrás contadores globales de células vivas evaluando la diferencia entrevolatileyAtomicInteger. - En Rust: Reimplementarás la simulación con
std::thread::spawn, resolviendo los retos de compartición de estado que impone el compilador medianteArc<Mutex<Grid>>.
3. Conceptos previos: volatile frente a tipos atómicos
Antes de abordar los ejercicios de la práctica, es necesario clarificar la diferencia entre visibilidad en memoria y atomicidad de operaciones.
A. La palabra clave volatile (Visibilidad de memoria)
En procesadores multinúcleo modernos, cada núcleo mantiene copias locales de las variables en sus registros de CPU o en cachés ultrarrápidas (L1/L2). Si un hilo modifica una variable normal en memoria compartida, los cambios pueden tardar un tiempo en reflejarse en la memoria RAM principal, por lo que otros hilos ejecutándose en distintos núcleos continuarán leyendo el valor antiguo (falta de visibilidad).
Al declarar una variable con la palabra clave volatile:
- Se obliga a la máquina virtual (JVM) o al compilador a leer y escribir la variable siempre directamente en la memoria principal (RAM).
- Se garantiza visibilidad inmediata: cualquier actualización realizada por un hilo es visible de forma instantánea para todos los demás hilos.
volatile asegura que todos lean el valor más reciente de la RAM, pero no evita las condiciones de carrera en operaciones compuestas.
Una instrucción como count++ no es atómica: requiere 3 pasos independientes (1. Leer count, 2. Incrementar en registro, 3. Escribir count). Si dos hilos ejecutan volatileCounter++ al mismo tiempo, ambos leerán el mismo valor inicial y se perderá uno de los incrementos.
B. Tipos atómicos (AtomicInteger / AtomicUsize)
Para realizar modificaciones seguras sin necesidad de utilizar cerrojos pesados (mutex), las bibliotecas estándar de lenguajes de alto nivel ofrecen tipos atómicos (como AtomicInteger en Java o AtomicUsize en Rust).
- Los tipos atómicos se fundamentan en instrucciones indivisibles del hardware de la CPU, como
Compare-And-Swap(CAS). - Operaciones como
atomicCounter.incrementAndGet()en Java ofetch_add()en Rust ejecutan el ciclo lectura-modificación-escritura en un único paso atómico indivisible en el bus de memoria, garantizando la corrección absoluta sin pérdida de incrementos.
4. Código base de la práctica
Descarga el paquete comprimido de partida:
📥 game_of_life_java_rust.zip
El paquete contiene la estructura limpia del autómata en Java y un proyecto inicial de Cargo en Rust:
java/GameOfLifeBase.java: Plantilla en Java con la estructura del tablero y la simulación.rust/game_of_life_rust/: Proyecto Rust en Cargo (Cargo.tomlysrc/main.rs).
5. Parte A: Concurrencia en la JVM (Java)
En esta primera parte portarás el cálculo del autómata celular a Java aprovechando la infraestructura de concurrencia del paquete java.util.concurrent.
Ejercicio 1: Paralelización mediante ExecutorService (ThreadPool)
- Modifica el método
stepParallel()enGameOfLifeBase.javapara dividir el total de filas del tablero () entre los hilos del pool. - Utiliza un
ExecutorServiceconfigurado medianteExecutors.newFixedThreadPool(numThreads). - Envía las tareas de cálculo de franjas de filas como instancias
Runnableconexecutor.submit().
Ejercicio 2: Estudio empírico de Visibilidad vs. Atomicidad (volatile vs. AtomicInteger)
En la plantilla se definen dos contadores globales que registran las células vivas nacidas o supervivientes a lo largo de las generaciones:
public static AtomicInteger atomicCounter = new AtomicInteger(0);
public static volatile int volatileCounter = 0;
- En el bucle interno de cálculo, incrementa ambos contadores en cada célula activa:
atomicCounter.incrementAndGet();volatileCounter++;
- Ejecuta la simulación con 4 y 8 hilos durante 50 generaciones.
Cuestiones a responder en la memoria:
- Compara el valor final de
atomicCounterfrente avolatileCounter. ¿Por quévolatileCounterno alcanza el valor correcto a pesar de estar marcado con la palabra clavevolatile? - Apoyándote en la sección teórica previa, explica con tus palabras la diferencia entre visibilidad en memoria RAM y atomicidad de operaciones.
6. Parte B: Concurrencia garantizada en compilación (Rust)
Rust utiliza su sistema de propiedad (ownership), préstamos (borrowing) y los traits Send y Sync para garantizar que no existan condiciones de carrera en programas compilados.
Ejercicio 3: Diagnóstico del Borrow Checker (rustc)
- Abre el proyecto en
rust/game_of_life_rust/. - Intenta compartir una referencia directamente entre varios hilos creados con
std::thread::spawnsin utilizar envoltorios de sincronización:// Intento de compartir el tablero directamente entre hilosthread::spawn(move || {// Acceso al tablero compartido}); - Ejecuta
cargo checky observa el rechazo del compilador.
Cuestión a responder en la memoria:
- Adjunta el mensaje de error emitido por
rustce explica qué regla del Borrow Checker se viola al intentar compartir una estructura mutable entre varios hilos independientes.
Ejercicio 4: Implementación idiomática con Arc<Mutex<Grid>>
- Resuelve la compartición segura del tablero envolviendo la estructura en un puntero atómico con conteo de referencias (
Arc) y un cerrojo (Mutex):let curr_grid = Arc::new(Mutex::new(initial_grid)); - Para cada hilo creado, clona el puntero de referencia atómica (
Arc::clone(&curr_grid)) y pasa la propiedad a la clausura conmove. - Adquiere el acceso al tablero mediante
lock().unwrap()de forma segura dentro de la tarea del hilo. - Utiliza el tipo
std::sync::atomic::AtomicUsizepara contabilizar de forma segura el número de células vivas. - Verifica que el programa compila y se ejecuta limpiamente a 10 FPS con
cargo run.
7. Criterios de evaluación y entrega
La entrega se realizará en la plataforma pracdlsi en un archivo comprimido denominado p1_3.tgz.
El archivo comprimido debe incluir:
memoria.pdf(omemoria.md): Informe documentado respondiendo a las cuestiones de atomicidad en Java y adjuntando el análisis del error de compilación de Rust.GameOfLife.java: Código fuente Java refactorizado y libre de errores.game_of_life_rust/: Proyecto Cargo completo funcional y compilable limpiamente concargo run.