Arc

Según se ha visto, para permitir que una variable tuviera más de un dueño de forma segura, se utiliza Rc. Si se quiere hacer lo mismo compartiendo la propiedad de una variable entre diferentes hilos, se debe usar Arc. Arc significa: contador de referencias atómico. En este contexto, atómico se refiere a que solo un proceso de los existentes puede escribir en él cada vez. Esto es importante ya que si dos hilos escribieran a la vez, podrían darse resultados erróneos. Por ejemplo, si se pudiera hacer esto en Rust:

#![allow(unused)]
fn main() {
// 🚧
let mut x = 10;

for i in 0..10 { // Hilo 1
    x += 1
}
for i in 0..10 { // Hilo 2
    x += 1
}
}

Si el hilo 1 y el hilo 2 comienzan a la vez y se permitiera escribir simultáneamente, podría darse lo siguiente:

  • El hilo 1 lee 10 y escribe 11. Posteriormente, el hilo 2 lee 11 y escribe 12. Esto no causa problema.
  • El hilo 1 lee 12 y a la vez el hilo 2 lee 12. El hilo 1 escribe 13 y el hilo 2 escribe 13. Se ha perdido un incremento, lo que es un problema grave.

El tipo Arc se asegura de que esto no suceda y es lo que se debe usar para compartir valores entre hilos. Si no hay hilos, es suficiente con usar Rc que, además, es ligeramente más rápido.

Para poder modificar los valores de un Arc no es suficiente con él. Se necesita envolver los datos en un Mutex que es lo que se compartirá entre hilos con Arc.

En el siguiente ejemplo, se va a usar un Mutex dentro de un Arc para modificar un valor de un número entre hilos. En primer lugar, se muestra el primer hilo:

fn main() {

    let handle = std::thread::spawn(|| {
        println!("The thread is working!") // Just testing the thread
    });

    handle.join().unwrap(); // Make the thread wait here until it is done
    println!("Exiting the program");
}

Por ahora, solo imprime:

The thread is working!
Exiting the program

Bien, a continuación se incluye un bucle en el hilo:

fn main() {

    let handle = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("The thread is working!")
        }
    });

    handle.join().unwrap();
    println!("Exiting the program");
}

También funciona, su ejecución resulta en:

The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program

Ahora se crea otro hilo que hará lo mismo. La forma en que se ordena la impresión puede ser diferente cada vez, según sea la ejecución paralela de ambos hilos. Se ejecutan de forma concurrente, que significa que se ejecutan a la vez.

fn main() {

    let thread1 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 1 is working!")
        }
    });

    let thread2 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 2 is working!")
        }
    });

    thread2.join().unwrap();
    thread1.join().unwrap();
    println!("Exiting the program");
}

Que podría imprimir diferentes resultados cada vez (dependiendo de la velocidad de ejecución de cada hilo), como por ejemplo:

Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Exiting the program

Ahora se trata de que cada hilo cambie el valor de my_number que puede ser un i32. Para que se pueda compartir entre hilos, se debe definir así:

#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));
}

Después, se deben clonar (que realmente solo clona el puntero), para pasar cada clon a cada hilo.

#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));

let my_number1 = Arc::clone(&my_number); // Este clon va al hilo 1
let my_number2 = Arc::clone(&my_number); // Este clon va al hilo 2
}

Ahora se pueden mover (move) a cada hilo:

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));

    let my_number1 = Arc::clone(&my_number);
    let my_number2 = Arc::clone(&my_number);

    let thread1 = std::thread::spawn(move || { // El clon va al hilo 1
        for _ in 0..10 {
            *my_number1.lock().unwrap() +=1; // Bloquea el Mutex y cambia el valor
        }
    });

    let thread2 = std::thread::spawn(move || { // El clon va al hilo 2
        for _ in 0..10 {
            *my_number2.lock().unwrap() += 1;
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Value is: {:?}", my_number);
    println!("Exiting the program");
}

El programa da como resultado:

Value is: Mutex { data: 20 }
Exiting the program

Para simplificar el código, se puede unificar el código de cada hilo (ya que es idéntico):

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));
    let mut handle_vec = vec![]; // los JoinHandles irán aquí

    for _ in 0..2 { // se hace dos veces
        let my_number_clone = Arc::clone(&my_number); // se clona antes de iniciar el hilo
        let handle = std::thread::spawn(move || { // se mueve el clon
            for _ in 0..10 {
                *my_number_clone.lock().unwrap() += 1;
            }
        });
        handle_vec.push(handle); // se guarda el manejador para poder hacer join cuando estén lanzados los dos hilos.
            //si no lo hiciéramos, este manejador se perdería
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // se llama al join de todos los hilos lanzados (dos, en este caso)
    println!("{:?}", my_number);
}

Esto imprime: { data: 20 }.

Aunque parece complejo Arc<Mutex<AlgunTipo>> es algo que se usa mucho en Rust y se vuelve natural. Siempre se puede escribir el código para que quede más claro. A continuación, se muestra el mismo código, pero añadiendo un use y dos funciones. Así el código de main() queda conciso y claro:

use std::sync::{Arc, Mutex};
use std::thread::spawn; // Así solo hace falta escribir spawn

fn make_arc(number: i32) -> Arc<Mutex<i32>> { // una función para crear un Mutex en un Arc
    Arc::new(Mutex::new(number))
}

fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> { // para crear clones
    Arc::clone(&input)
}

// Ahora main() se lee más fácil
fn main() {
    let mut handle_vec = vec![]; // los manejadores de hilos se guardan aquí
    let my_number = make_arc(0);

    for _ in 0..2 {
        let my_number_clone = new_clone(&my_number);
        let handle = spawn(move || {
            for _ in 0..10 {
                let mut value_inside = my_number_clone.lock().unwrap();
                *value_inside += 1;
            }
        });
        handle_vec.push(handle);    // se guarda el manejador
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // se espera a la finalización de los hilos

    println!("{:?}", my_number);
}

En todo caso, siempre se puede reescribir el código que parezca difícl de leer.