Los tipos atómicos en Rust permiten la mutación de datos compartidos entre hilos sin la sobrecarga de un Mutex. Mientras que un Mutex utiliza bloqueos a nivel del sistema operativo para garantizar la exclusión mutua, los atómicos aprovechan instrucciones directas del procesador (como Compare-and-Swap o CAS) para realizar operaciones de lectura y escritura de forma indivisible. Esto es fundamental en programación lock-free, donde se busca maximizar el rendimiento reduciendo la contención y el tiempo de espera de los hilos.

El desafío de usar atómicos reside en el parámetro Ordering (modelo de memoria). Debido a las optimizaciones de compilación y de hardware (reordenamiento de instrucciones), no basta con que una operación sea atómica; es necesario garantizar la visibilidad de la memoria entre hilos. Si se utiliza Ordering::Relaxed, solo se garantiza la atomicidad de la variable, pero otros hilos podrían ver cambios en variables adyacentes en un orden incorrecto. Para sincronización de datos (como un flag que indica que un buffer está listo), se requiere Acquire y Release, que establecen una relación de “happens-before”. Para una consistencia total y evitar cualquier tipo de reordenamiento global, se utiliza SeqCst (Sequential Consistency), que es el más seguro pero el más costoso en términos de rendimiento.

El uso incorrecto de estos tipos suele derivar en race conditions lógicas. Aunque la variable no corrompa su valor interno (gracias a la atomicidad), la lógica de negocio puede fallar si un hilo asume que un estado es válido basándose en una lectura Relaxed que no ha sincronizado la memoria de los datos que la acompañan.

use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

fn main() {
    // AtomicUsize para un contador compartido sin Mutex
    let counter = Arc::new(AtomicUsize::new(0));
    // AtomicBool para señalizar la terminación de los hilos (flag de parada)
    let is_running = Arc::new(AtomicBool::new(true));

    let mut handles = vec![];

    for i in 0..4 {
        let c = Arc::clone(&counter);
        let r = Arc::clone(&is_running);

        let handle = thread::spawn(move || {
            while r.load(Ordering::Acquire) {
                // Implementación manual de un incremento usando compare_exchange (CAS)
                // Esto simula cómo funciona un fetch_add internamente para asegurar
                // que la lógica dependa del valor actual.
                let mut current = c.load(Ordering::Relaxed);
                loop {
                    let new_val = current + 1;
                    // Intentamos cambiar el valor solo si 'current' sigue siendo el valor que leímos
                    match c.compare_exchange(
                        current,
                        new_val,
                        Ordering::SeqCst, // Éxito: garantiza visibilidad global
                        Ordering::Relaxed, // Fallo: no necesitamos sincronizar si falló
                    ) {
                        Ok(_) => {
                            // Incremento exitoso
                            println!("Hilo {} incrementó a: {}", i, new_val);
                            break;
                        }
                        Err(actual) => {
                            // El valor cambió antes de que pudiéramos actualizarlo.
                            // Actualizamos 'current' con el valor real obtenido y reintentamos.
                            current = actual;
                        }
                    }
                }
                thread::sleep(Duration::from_millis(10));
            }
        });
        handles.push(handle);
    }

    // Dejar correr los hilos por un tiempo
    thread::sleep(Duration::from_millis(100));
    
    // Señalizar a los hilos que deben detenerse
    println!("Deteniendo hilos...");
    is_running.store(false, Ordering::Release);

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Contador final: {}", counter.load(Ordering::SeqCst));
}

Explicación del Código

  1. counter: AtomicUsize: Un contador atómico inicializado en 0. Se utiliza para demostrar operaciones de comparación y intercambio.
  2. is_running: AtomicBool: Un flag atómico utilizado para controlar el ciclo de vida de los hilos de forma segura.
  3. r.load(Ordering::Acquire): En el bucle while, se usa Acquire para asegurar que, si is_running es false, este hilo vea todas las escrituras realizadas por el hilo principal antes de que este ejecutara el store.
  4. c.load(Ordering::Relaxed): Se utiliza Relaxed para la carga inicial de current porque la sincronización real se gestiona en el siguiente paso mediante compare_exchange.
  5. c.compare_exchange(...): Esta es la operación núcleo.
    • El primer parámetro es el valor esperado (current).
    • El segundo es el nuevo valor (new_val).
    • El tercer parámetro es Ordering::SeqCst para la operación exitosa, asegurando que el incremento sea visible para todos los hilos de forma inmediata y consistente.
    • El cuarto parámetro es Ordering::Relaxed para el caso de error. Si la comparación falla, no necesitamos sincronizar la memoria, solo necesitamos el nuevo valor real que la función nos devuelve en Err(actual).
  6. is_running.store(false, Ordering::Release): Se usa Release para garantizar que todas las operaciones de memoria realizadas por el hilo principal antes de este comando sean visibles para los hilos que realicen un load(Ordering::Acquire) sobre la misma variable.
  7. counter.load(Ordering::SeqCst): Se usa SeqCst al final para asegurar que leemos el valor más reciente tras la finalización de todos los hilos, garantizando un orden total.

82