Sincronización

Cuando los datos son compartidos entre hilos, es necesario sincronizar el acceso de lectura y escritura a los datos para evitar la corrupción. En C#, se ofrece la palabra clave lock como un primitivo de sincronización (que se desenrolla en el uso seguro de excepciones de Monitor de .NET):

using System;
using System.Threading;

var dataLock = new object();
var data = 0;
var threads = new List<Thread>();

for (var i = 0; i < 10; i++)
{
    var thread = new Thread(() =>
    {
        for (var j = 0; j < 1000; j++)
        {
            lock (dataLock)
                data++;
        }
    });
    threads.Add(thread);
    thread.Start();
}

foreach (var thread in threads)
    thread.Join();

Console.WriteLine(data);

En Rust, uno debe hacer uso explícito de estructuras de concurrencia como Mutex:

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

fn main() {
    let data = Arc::new(Mutex::new(0)); // (1)

    let mut threads = vec![];
    for _ in 0..10 {
        let data = Arc::clone(&data); // (2)
        let thread = thread::spawn(move || { // (3)
            for _ in 0..1000 {
                let mut data = data.lock().unwrap();
                *data += 1; // (4)
            }
        });
        threads.push(thread);
    }

    for thread in threads {
        thread.join().unwrap();
    }

    println!("{}", data.lock().unwrap());
}

Algunas cosas a tener en cuenta:

  • Dado que la propiedad de la instancia de Mutex y, a su vez, los datos que protege serán compartidos por múltiples hilos, se envuelve en un Arc (1). Arc proporciona recuento de referencias atómico, que se incrementa cada vez que se clona (2) y se decrementa cada vez que se elimina. Cuando el recuento alcanza cero, se elimina el mutex y, por lo tanto, los datos que protege. Esto se discute con más detalle en Gestión de Memoria.

  • La closure para cada hilo recibe la propiedad (3) de la referencia clonada (2).

  • El código similar a un puntero, que es *data += 1 (4), no es un acceso a puntero inseguro incluso si parece serlo. Está actualizando los datos envueltos en el mutex guard.

A diferencia de la versión de C#, donde se puede volver inseguro para hilos al comentar la declaración lock, la versión de Rust se negará a compilar si se cambia de alguna manera (por ejemplo, al comentar partes) que la vuelva insegura para hilos. Esto demuestra que escribir código seguro para hilos es responsabilidad del desarrollador en C# y .NET mediante el uso cuidadoso de estructuras sincronizadas, mientras que en Rust, uno puede confiar en el compilador.

El compilador puede ayudar porque las estructuras de datos en Rust están marcadas por traits especiales (ver Interfaces): Sync y Send. Sync indica que las referencias a las instancias de un tipo son seguras para compartir entre hilos. Send indica que es seguro enviar instancias de un tipo a través de los límites de los hilos. Para obtener más información, consulta el capítulo "Concurrencia sin miedo" del libro de Rust.