Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Utilizzo dei Thread per Eseguire Codice Simultaneamente

Nei moderni sistemi operativi, il codice di un programma in esecuzione viene eseguito in un processo, e il sistema operativo gestirà più processi contemporaneamente. All’interno di un programma, è possibile avere anche parti indipendenti che vengono eseguite contemporaneamente. Le funzionalità che eseguono queste parti indipendenti sono chiamate thread. Ad esempio, un server web potrebbe avere più thread in modo da poter rispondere a più di una richiesta contemporaneamente.

Splitting the computation del tuo programma in multiple threads per eseguire multiple tasks contemporaneamente può migliorare le prestazioni, ma aggiunge anche complessità. Poiché i thread possono essere eseguiti contemporaneamente, non esiste alcuna garanzia intrinseca sull’ordine in cui parti del tuo codice su thread diversi verranno eseguite. Questo può portare a problemi come:

  • Race conditions, dove i thread accedono a dati o risorse in un ordine inconsistente
  • Deadlocks, dove due thread stanno aspettando l’uno dell’altro, impedendo ad entrambi di continuare
  • Bug che si verificano solo in determinate situazioni e sono difficili da riprodurre e correggere in modo affidabile

Rust cerca di mitigare gli effetti negativi dell’utilizzo dei thread, ma la programmazione in un contesto multithread richiede comunque un pensiero attento e richiede una struttura di codice diversa da quella dei programmi in esecuzione su un singolo thread.

Creating a New Thread with spawn

Per creare un nuovo thread, chiamiamo la funzione thread::spawn e le passiamo una closure contenente il codice che vogliamo eseguire nel nuovo thread. L’esempio nel Listing 16-1 stampa del testo da un thread principale e altro testo da un nuovo thread:

rust

use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});

for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}

Si noti che quando il thread principale di un programma Rust completa, tutti i thread creati sono chiusi, che siano o meno terminati. L’output di questo programma potrebbe essere un po’ diverso ogni volta, ma assomiglierà a quanto segue:

plaintext

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

Le chiamate a thread::sleep forzano un thread a interrompere la sua esecuzione per una breve durata, consentendo a un altro thread di eseguire. I thread probabilmente si alterneranno, ma non è garantito: dipende da come il sistema operativo pianifica i thread.

Se eseguiamo questo codice e vediamo solo l’output dal thread principale, o non vediamo sovrapposizioni, prova ad aumentare i numeri negli intervalli per creare più opportunità per il sistema operativo di passare tra i thread.

Waiting for All Threads to Finish Using join Handles

Il codice nel Listing 16-1 non solo interrompe prematuramente il thread creato nella maggior parte dei casi a causa della fine del thread principale, ma anche perché non c’è alcuna garanzia sull’ordine in cui vengono eseguiti i thread, non possiamo garantire che il thread creato venga eseguito affatto!

Possiamo risolvere il problema del thread creato che non viene eseguito o termina prematuramente salvando il valore di ritorno di thread::spawn in una variabile. Il tipo di ritorno di thread::spawn è JoinHandle. Un JoinHandle è un valore posseduto che, quando chiamiamo il metodo join su di esso, attende che il suo thread termini. Il Listing 16-2 mostra come utilizzare il JoinHandle del thread creato nel Listing 16-1 e chiamare join per assicurarsi che il thread creato termini prima che il main esca:

rust

use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
}); for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}

handle.join().unwrap();
}

La chiamata a join sul handle blocca il thread attualmente in esecuzione fino a quando il thread rappresentato dall’handle termina. Bloccare un thread significa che il thread viene impedito di eseguire lavoro o di uscire. Poiché abbiamo messo la chiamata a join dopo il ciclo for del thread principale, eseguire il Listing 16-2 dovrebbe produrre un output simile a questo:

plaintext

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

I due thread continuano a alternarsi, ma il thread principale attende a causa della chiamata a handle.join() e non termina fino a quando il thread creato non è finito.

Ma vediamo cosa succede quando invece spostiamo handle.join() prima del ciclo for nel main, così:

rust

use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
}); handle.join().unwrap();

for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}

Il thread principale attende che il thread creato termini e quindi esegue il suo ciclo for, quindi l’output non sarà più interlacciato, come mostrato qui:

plaintext

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

Piccoli dettagli, come dove viene chiamato join, possono influenzare se i tuoi thread vengono eseguiti contemporaneamente.

Utilizzo di Closures con la Keyword move nei Thread

Spesso useremo la parola chiave move con closures passate a thread::spawn perché la chiusura prenderà quindi il possesso dei valori che utilizza dall’ambiente, trasferendo quindi la proprietà di quei valori da un thread a un altro. Nella sezione “Catturare Riferimenti o Trasferire la Proprietà” del Capitolo 13, abbiamo discusso di move nel contesto delle closures. Ora, ci concentreremo di più sull’interazione tra move e thread::spawn.

Nota nel Listing 16-1 che la chiusura che passiamo a thread::spawn non prende argomenti: non stiamo usando alcun dato dal thread principale nel codice del thread creato. Per utilizzare dati dal thread principale nel thread creato, la chiusura del thread creato deve catturare i valori di cui ha bisogno. Il Listing 16-3 mostra un tentativo di creare un vettore nel thread principale e usarlo nel thread creato. Tuttavia, questo non funzionerà ancora, come vedremo tra un momento.

rust

use std::thread; fn main() {
let v = vec![1, 2, 3]; let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});

handle.join().unwrap();
}

La chiusura usa v, quindi catturerà v e lo renderà parte dell’ambiente della chiusura. Poiché thread::spawn esegue questa chiusura in un nuovo thread, dovremmo essere in grado di accedere a v all’interno di quel nuovo thread. Ma quando compiliamo questo esempio, otteniamo il seguente errore:

plaintext

$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {:?}", v);
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++

Per ulteriori informazioni su questo errore, prova `rustc --explain E0373`.
error: could not compile `threads` due to previous error

Rust infers come catturare v, e poiché println! ha bisogno solo di un riferimento a v, la chiusura cerca di prendere in prestito v. Tuttavia, c’è un problema: Rust non può sapere per quanto tempo verrà eseguito il thread creato, quindi non sa se il riferimento a v sarà sempre valido.

Il Listing 16-4 fornisce uno scenario che è più probabile abbia un riferimento a v che non sarà valido:

rust

use std::thread; fn main() {
let v = vec![1, 2, 3]; let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
}); drop(v); // oh no!

handle.join().unwrap();
}

Se Rust ci permettesse di eseguire questo codice, c’è la possibilità che il thread creato venga immediatamente messo in background senza essere eseguito affatto. Il thread creato ha un riferimento a v all’interno, ma il thread principale abbandona immediatamente v, utilizzando la funzione drop che abbiamo discusso nel Capitolo 15. Quindi, quando il thread creato inizia ad eseguire, v non è più valido, quindi un riferimento ad esso è anche non valido. Oh no!

Per risolvere l’errore del compilatore nel Listing 16-3, possiamo utilizzare il consiglio del messaggio di errore:

plaintext

help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++

Aggiungendo la parola chiave move prima della chiusura, costringiamo la chiusura a prendere il possesso dei valori che sta utilizzando anziché consentire a Rust di inferire che dovrebbe prendere in prestito i valori. La modifica al Listing 16-3 mostrata nel Listing 16-5 compilerà ed eseguirà come intendiamo:

rust

use std::thread; fn main() {
let v = vec![1, 2, 3]; let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});

handle.join().unwrap();
}

Potremmo essere tentati di provare la stessa cosa per correggere il codice nel Listing 16-4 dove il thread principale ha chiamato drop usando una chiusura move. Tuttavia, questa correzione non funzionerà perché ciò che il Listing 16-4 sta cercando di fare è vietato per un motivo diverso. Se aggiungessimo move alla chiusura, sposteremmo v nell’ambiente della chiusura e non potremmo più chiamare drop su di esso nel thread principale. Otterremmo invece questo errore del compilatore:

plaintext

$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {:?}", v);
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move

Per ulteriori informazioni su questo errore, prova rustc --explain E0382.

Le regole di ownership di Rust ci hanno salvato di nuovo! Abbiamo ottenuto un errore dal codice nel Listing 16-3 perché Rust era conservativo e prendeva in prestito solo v per il thread, il che significava che il thread principale teoricamente poteva invalidare il riferimento del thread creato. Dicendo a Rust di spostare la proprietà di v al thread creato, garantiamo a Rust che il thread principale non utilizzerà più v. Se cambiamo il Listing 16-4 allo stesso modo, stiamo violando le regole di ownership quando proviamo a usare v nel thread principale. La parola chiave move annulla la predefinizione conservativa di Rust di prendere in prestito; non ci consente di violare le regole di ownership.

Con una comprensione di base dei thread e dell’API dei thread, vediamo cosa possiamo fare con i thread.

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *