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.