Saltar al contenido principal

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​

  1. 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.
  2. 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.
  3. Distinguir visibilidad de atomicidad: Comprender el papel de la palabra clave volatile frente a los tipos atómicos (AtomicInteger) bajo accesos concurrentes en memoria.
  4. 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 (70×1570 \times 15) 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 entre volatile y AtomicInteger.
  • En Rust: Reimplementarás la simulación con std::thread::spawn, resolviendo los retos de compartición de estado que impone el compilador mediante Arc<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.
¡Atención! volatile NO garantiza atomicidad

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 o fetch_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.toml y src/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)​

  1. Modifica el método stepParallel() en GameOfLifeBase.java para dividir el total de filas del tablero (70×1570 \times 15) entre los hilos del pool.
  2. Utiliza un ExecutorService configurado mediante Executors.newFixedThreadPool(numThreads).
  3. Envía las tareas de cálculo de franjas de filas como instancias Runnable con executor.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;
  1. En el bucle interno de cálculo, incrementa ambos contadores en cada célula activa:
    atomicCounter.incrementAndGet();
    volatileCounter++;
  2. Ejecuta la simulación con 4 y 8 hilos durante 50 generaciones.

Cuestiones a responder en la memoria:​

  • Compara el valor final de atomicCounter frente a volatileCounter. ¿Por qué volatileCounter no alcanza el valor correcto a pesar de estar marcado con la palabra clave volatile?
  • 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)​

  1. Abre el proyecto en rust/game_of_life_rust/.
  2. Intenta compartir una referencia directamente entre varios hilos creados con std::thread::spawn sin utilizar envoltorios de sincronización:
    // Intento de compartir el tablero directamente entre hilos
    thread::spawn(move || {
    // Acceso al tablero compartido
    });
  3. Ejecuta cargo check y observa el rechazo del compilador.

Cuestión a responder en la memoria:​

  • Adjunta el mensaje de error emitido por rustc e 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>>​

  1. 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));
  2. Para cada hilo creado, clona el puntero de referencia atómica (Arc::clone(&curr_grid)) y pasa la propiedad a la clausura con move.
  3. Adquiere el acceso al tablero mediante lock().unwrap() de forma segura dentro de la tarea del hilo.
  4. Utiliza el tipo std::sync::atomic::AtomicUsize para contabilizar de forma segura el número de células vivas.
  5. 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:

  1. memoria.pdf (o memoria.md): Informe documentado respondiendo a las cuestiones de atomicidad en Java y adjuntando el análisis del error de compilación de Rust.
  2. GameOfLife.java: Código fuente Java refactorizado y libre de errores.
  3. game_of_life_rust/: Proyecto Cargo completo funcional y compilable limpiamente con cargo run.