SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Utilizzo del Passaggio Messaggi per Trasferire Dati tra Thread
Un approccio sempre più popolare per garantire la sicurezza della concorrenza è il passaggio messaggi, dove i thread o gli attori comunicano inviandosi messaggi contenenti dati. Ecco l’idea in uno slogan dalla documentazione del linguaggio Go: “Non comunicare condividendo la memoria; invece, condividi la memoria comunicando.”
Per realizzare la concorrenza mediante il passaggio dei messaggi, la libreria standard di Rust fornisce un’implementazione dei canali. Un canale è un concetto di programmazione generale mediante il quale i dati vengono inviati da un thread all’altro.
Puoi immaginare un canale nella programmazione come un canale direzionale d’acqua, come un ruscello o un fiume. Se metti qualcosa come una papera di gomma in un fiume, essa viaggerà a valle fino alla fine del corso d’acqua.
Un canale ha due metà: un trasmettitore e un ricevitore. La metà del trasmettitore è la posizione a monte dove metti le papere di gomma nel fiume, e la metà del ricevitore è dove la papera di gomma finisce a valle. Una parte del tuo codice chiama i metodi sul trasmettitore con i dati che vuoi inviare, e un’altra parte controlla l’estremità ricevente per i messaggi in arrivo. Un canale viene detto chiuso se viene eliminato uno dei due lati, trasmettitore o ricevitore.
Qui, arriveremo a un programma che ha un thread per generare valori e inviarli giù per un canale, e un altro thread che riceverà i valori e li stamperà. Invieremo valori semplici tra i thread usando un canale per illustrare la funzionalità. Una volta familiarizzato con la tecnica, potresti utilizzare i canali per qualsiasi thread che necessiti di comunicare tra loro, come un sistema di chat o un sistema in cui molti thread eseguono parti di un calcolo e inviano le parti a un thread che aggrega i risultati.
Prima, nel Listato 16-6, creeremo un canale ma non faremo nulla con esso. Nota che questo non verrà ancora compilato perché Rust non sa che tipo di valori vogliamo inviare attraverso il canale.
rust
// [Questo codice non compila!]
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
Listato 16-6: Creazione di un canale e assegnazione delle due metà a tx e rx
Creiamo un nuovo canale usando la funzione mpsc::channel; mpsc sta per multiple producer, single consumer. In breve, il modo in cui la libreria standard di Rust implementa i canali significa che un canale può avere più estremità trasmettenti che producono valori ma solo un’estremità ricevente che consuma quei valori. Immagina vari corsi d’acqua che confluiscono in un unico grande fiume: tutto ciò che viene inviato giù da uno qualsiasi dei corsi d’acqua finirà in un unico fiume alla fine. Cominceremo con un singolo produttore per ora, ma aggiungeremo produttori multipli quando otterremo questo esempio funzionante.
La funzione mpsc::channel restituisce una tupla, il primo elemento della quale è l’estremità trasmettitore e il secondo elemento è l’estremità ricevente. Le abbreviazioni tx e rx sono tradizionalmente usate in molti campi per trasmettitore e ricevitore rispettivamente, quindi nominiamo le nostre variabili in questo modo per indicare ciascuna estremità. Utilizziamo un’istruzione let con un pattern che destruttura le tuple; discuteremo dell’uso di pattern nelle istruzioni let e della destrutturazione nel Capitolo 18. Per ora, sappi che utilizzare un’istruzione let in questo modo è un approccio comodo per estrarre i pezzi della tupla restituita da mpsc::channel.
Spostiamo l’estremità trasmettitore in un thread creato e facciamola inviare una stringa in modo che il thread creato stia comunicando con il thread principale, come mostrato nel Listato 16-7. Questo è simile a mettere una papera di gomma nel fiume a monte o inviare un messaggio di chat da un thread all’altro.
rust
use std::sync::mpsc;
use std::thread; fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
}
Listato 16-7: Spostamento di tx in un thread creato e invio di “hi”
Ancora una volta, usiamo thread::spawn per creare un nuovo thread e quindi usiamo move per spostare tx nella chiusura in modo che il thread creato possieda tx. Il thread creato deve possedere il trasmettitore per poter inviare messaggi attraverso il canale. Il trasmettitore ha un metodo send che prende il valore che vogliamo inviare. Il metodo send restituisce un tipo Result<T, E>, quindi se il ricevitore è già stato eliminato e non c’è alcun posto dove inviare un valore, l’operazione di invio restituirà un errore. In questo esempio, chiamiamo unwrap per generare un panic in caso di errore. Ma in un’applicazione reale, lo gestiremmo correttamente: torna al Capitolo 9 per rivedere le strategie per una corretta gestione degli errori.
Nel Listato 16-8, otterremo il valore dal ricevitore nel thread principale. Questo è simile al recupero della papera di gomma dall’acqua alla fine del fiume o alla ricezione di un messaggio di chat.
rust
use std::sync::mpsc;
use std::thread; fn main() {
let (tx, rx) = mpsc::channel(); thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
Listato 16-8: Ricezione del valore “hi” nel thread principale e stampa
Il ricevitore ha due metodi utili: recv e try_recv. Stiamo usando recv, abbreviazione di receive, che bloccherà l’esecuzione del thread principale e aspetterà fino a quando non verrà inviato un valore giù per il canale. Una volta inviato un valore, recv lo restituirà in un Result<T, E>. Quando il trasmettitore chiude, recv restituirà un errore per segnalare che non arriveranno più valori.
Il metodo try_recv non blocca, ma restituirà invece immediatamente un Result<T, E>: un valore Ok che contiene un messaggio se uno è disponibile e un valore Err se non ci sono messaggi questa volta. Usare try_recv è utile se questo thread ha altro lavoro da fare mentre aspetta i messaggi: potremmo scrivere un ciclo che chiama try_recv ogni tanto, gestisce un messaggio se uno è disponibile e altrimenti svolge altro lavoro per un po’ prima di controllare di nuovo.
Abbiamo usato recv in questo esempio per semplicità; non abbiamo altro lavoro che il thread principale deve fare oltre ad aspettare i messaggi, quindi bloccare il thread principale è appropriato.
Quando eseguiamo il codice nel Listato 16-8, vedremo il valore stampato dal thread principale:
Got: hi
Perfetto! Canali e Trasferimento della Proprietà
Le regole di proprietà svolgono un ruolo vitale nell’invio dei messaggi perché ti aiutano a scrivere codice sicuro e concorrente. Prevenire errori nella programmazione concorrente è il vantaggio di pensare alla proprietà in tutto il tuo programma Rust. Facciamo un esperimento per mostrare come i canali e la proprietà lavorino insieme per prevenire problemi: cercheremo di utilizzare un valore val nel thread creato dopo averlo inviato giù per il canale. Prova a compilare il codice nel Listato 16-9 per vedere perché questo codice non è consentito:
rust
// [Questo codice non compila!]
use std::sync::mpsc;
use std::thread; fn main() {
let (tx, rx) = mpsc::channel(); thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {}", val);
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
Listato 16-9: Tentativo di utilizzare val dopo averlo inviato giù per il canale
Qui, proviamo a stampare val dopo averlo inviato giù per il canale tramite tx.send. Consentire questo sarebbe una cattiva idea: una volta che il valore è stato inviato a un altro thread, quel thread potrebbe modificarlo o eliminarlo prima che cerchiamo di utilizzare nuovamente il valore. Potenzialmente, le modifiche dell’altro thread potrebbero causare errori o risultati inaspettati a causa di dati inconsistently o inesistenti. Tuttavia, Rust ci restituisce un errore se proviamo a compilare il codice nel Listato 16-9:
less
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:31
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {}", val);
| ^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` due to previous error
Il nostro errore di concorrenza ha causato un errore in tempo di compilazione. La funzione send prende la proprietà del suo parametro, e quando il valore viene spostato, il ricevitore ne prende la proprietà. Questo ci impedisce di utilizzare accidentalmente nuovamente il valore dopo averlo inviato; il sistema di proprietà verifica che tutto sia a posto. Invio di Più Valori e Visualizzazione dell’Attesa del Ricevitore
Il codice nel Listato 16-8 è stato compilato ed eseguito, ma non ci ha mostrato chiaramente che due thread separati stavano comunicando tra loro attraverso il canale. Nel Listato 16-10 abbiamo apportato alcune modifiche che dimostreranno che il codice nel Listato 16-8 viene eseguito in modo concorrente: il thread creato invierà ora più messaggi e si metterà in pausa per un secondo tra ogni messaggio.
rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration; fn main() {
let (tx, rx) = mpsc::channel(); thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
]; for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {}", received);
}
}
Listato 16-10: Invio di più messaggi e pausa tra ciascuno
Questa volta, il thread creato ha un vettore di stringhe che vogliamo inviare al thread principale. Iteriamo su di essi, inviandoli singolarmente e mettendo in pausa tra ciascuno chiamando la funzione thread::sleep con un valore Duration di 1 secondo.
Nel thread principale, non stiamo più chiamando esplicitamente la funzione recv: invece, trattiamo rx come un iteratore. Per ogni valore ricevuto, lo stampiamo. Quando il canale è chiuso, l’iterazione terminerà.
Quando esegui il codice nel Listato 16-10, dovresti vedere l’output seguente con una pausa di 1 secondo tra ogni riga:
vbnet
Got: hi
Got: from
Got: the
Got: thread
Poiché non abbiamo alcun codice che si mette in pausa o ritarda nel ciclo for nel thread principale, possiamo dire che il thread principale sta aspettando di ricevere i valori dal thread creato. Creazione di Produttori Multipli Clonando il Trasmettitore
Prima abbiamo menzionato che mpsc era un acronimo per multiple producer, single consumer. Mettiamo mpsc in uso ed espandiamo il codice nel Listato 16-10 per creare thread multipli che inviano tutti valori allo stesso ricevitore. Possiamo farlo clonando il trasmettitore, come mostrato nel Listato 16-11:
rust
// --snip--
let (tx, rx) = mpsc::channel(); let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
]; for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
}); thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
]; for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
}); for received in rx {
println!("Got: {}", received);
}
// --snip--
Listato 16-11: Invio di più messaggi da parte di più produttori
Questa volta, prima di creare il primo thread creato, chiamiamo clone sul trasmettitore. Ciò ci darà un nuovo trasmettitore che possiamo passare al primo thread creato. Passiamo il trasmettitore originale a un secondo thread creato. Questo ci dà due thread, ognuno che invia messaggi diversi al singolo ricevitore.
Quando esegui il codice, il tuo output dovrebbe assomigliare a questo:
vbnet
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
Potresti vedere i valori in un altro ordine, a seconda del tuo sistema. Questo è ciò che rende la concorrenza interessante oltre che difficile. Se sperimenti con thread::sleep, dandogli vari valori nei diversi thread, ogni esecuzione sarà più non deterministica e creerà output diversi ogni volta.
Ora che abbiamo visto come funzionano i canali, diamo un’occhiata a un diverso metodo di concorrenza.