00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Thread

La programmazione concorrente è uno dei punti di forza di Rust. Grazie al sistema di ownership e ai controlli a tempo di compilazione, Rust garantisce la cosiddetta fearless concurrency: puoi scrivere codice concorrente senza temere data race o accessi non sicuri alla memoria.

Creare un Thread con std::thread::spawn

La funzione std::thread::spawn crea un nuovo thread di esecuzione. Accetta una closure che rappresenta il codice da eseguire nel thread:

use std::thread;

fn main() {
    thread::spawn(|| {
        println!("Ciao dal thread secondario!");
    });

    println!("Ciao dal thread principale!");
}

Attenzione: il thread principale potrebbe terminare prima che il thread secondario abbia completato l’esecuzione. Per evitare questo problema, utilizziamo il JoinHandle.

JoinHandle e join()

thread::spawn restituisce un JoinHandle<T>, che ci permette di attendere la terminazione del thread chiamando join():

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("Thread secondario: iterazione {}", i);
        }
        42 // valore di ritorno del thread
    });

    // Attende la fine del thread e ottiene il risultato
    let risultato = handle.join().unwrap();
    println!("Il thread ha restituito: {}", risultato);
}

Il metodo join() restituisce un Result<T, Box<dyn Any + Send>>. Se il thread è andato in panic, il Result contiene l’errore.

Move Closures con i Thread

Quando un thread ha bisogno di possedere i dati che utilizza, si usa la keyword move davanti alla closure:

use std::thread;

fn main() {
    let messaggio = String::from("Ciao dal thread!");

    let handle = thread::spawn(move || {
        // `messaggio` è stato spostato dentro il thread
        println!("{}", messaggio);
    });

    // println!("{}", messaggio); // ERRORE: messaggio è stato spostato
    handle.join().unwrap();
}

Senza move, il compilatore segnalerebbe un errore perché la closure potrebbe sopravvivere al dato preso in prestito.

Thread::sleep

Per mettere in pausa un thread si usa thread::sleep con una Duration:

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=3 {
            println!("Lavoro in corso... passo {}", i);
            thread::sleep(Duration::from_millis(500));
        }
    });

    handle.join().unwrap();
    println!("Lavoro completato!");
}

Thread Builder: Nome e Dimensione dello Stack

Con thread::Builder è possibile personalizzare il thread, assegnandogli un nome e una dimensione dello stack personalizzata:

use std::thread;

fn main() {
    let builder = thread::Builder::new()
        .name("worker-1".to_string())
        .stack_size(4 * 1024 * 1024); // 4 MB di stack

    let handle = builder.spawn(|| {
        let nome = thread::current().name().unwrap_or("anonimo").to_string();
        println!("Thread '{}' in esecuzione", nome);
    }).unwrap();

    handle.join().unwrap();
}

Il nome del thread è utile per il debugging e appare nei messaggi di panic. La dimensione dello stack personalizzata è necessaria quando si lavora con ricorsioni profonde o dati di grandi dimensioni sullo stack.

Scoped Threads con thread::scope

Introdotti stabilmente in Rust, gli scoped threads permettono di prendere in prestito dati dallo scope padre senza bisogno di move o Arc. Tutti i thread creati all’interno dello scope sono garantiti terminare prima che lo scope finisca:

use std::thread;

fn main() {
    let mut numeri = vec![1, 2, 3, 4, 5];

    thread::scope(|s| {
        // Questo thread può prendere in prestito `numeri` in modo immutabile
        s.spawn(|| {
            let somma: i32 = numeri.iter().sum();
            println!("Somma: {}", somma);
        });

        // Un altro thread che legge gli stessi dati
        s.spawn(|| {
            let max = numeri.iter().max().unwrap();
            println!("Massimo: {}", max);
        });
    });

    // Dopo lo scope, possiamo usare di nuovo `numeri`
    numeri.push(6);
    println!("Numeri aggiornati: {:?}", numeri);
}

Gli scoped threads sono particolarmente utili quando si vuole parallelizzare un’operazione senza trasferire la proprietà dei dati:

use std::thread;

fn main() {
    let dati = vec![1, 2, 3, 4, 5, 6, 7, 8];

    let mut risultati = vec![0; dati.len()];

    thread::scope(|s| {
        for (input, output) in dati.iter().zip(risultati.iter_mut()) {
            s.spawn(move || {
                *output = input * input; // calcolo parallelo
            });
        }
    });

    println!("Quadrati: {:?}", risultati);
}

Esempio Completo: Pool di Worker

Ecco un esempio piĂą complesso che combina diversi concetti:

use std::thread;
use std::time::Duration;

fn elabora_compito(id: u32, compito: &str) -> String {
    thread::sleep(Duration::from_millis(100));
    format!("Worker {} ha completato: {}", id, compito)
}

fn main() {
    let compiti = vec!["analisi", "report", "backup", "sync"];

    thread::scope(|s| {
        let mut handles = Vec::new();

        for (i, compito) in compiti.iter().enumerate() {
            let handle = s.spawn(move || {
                elabora_compito(i as u32, compito)
            });
            handles.push(handle);
        }

        for handle in handles {
            let risultato = handle.join().unwrap();
            println!("{}", risultato);
        }
    });

    println!("Tutti i compiti completati!");
}

Conclusione

I thread in Rust offrono un modello di concorrenza sicuro grazie al sistema di ownership. std::thread::spawn crea thread che possono restituire valori tramite JoinHandle, la keyword move trasferisce la proprietà dei dati, e gli scoped threads permettono di condividere riferimenti in modo sicuro. Combinando queste primitive con Mutex, Arc e i canali (trattati nelle prossime lezioni), è possibile costruire applicazioni concorrenti robuste e performanti.