|
Ad aumentare le possibilità di elaborazione che abbiamo con Rust eccoci ad un argomento che un po' non dico smentisca ma può sembrare in contrasto con quanto visto a proposito dell'ownership (e ci viene utile anche per la gestione di alcune situazioni in cui dovremmo ricorrere al lifetime). Con la sigla Rc sta per Reference Counted (oppure Reference Counter, dizione alternativa che si trova abbastanza diffusa) ovvero un contatore di riferimenti. Si tratta di uno smart pointer (ovvero una struttura che oltre a comportarsi come un puntatore tradizionale, fornisce anche funzionalità aggiuntive, in questo caso la gestione automatica della memoria, la contabilità delle referenze, la gestione della proprietà, e la protezione da accessi concorrenti) che consente di condividere la proprietà di un valore tra più parti del programma. È utile quando si ha bisogno di più proprietari per un dato e non possiamo determinare in anticipo quale parte del codice sarà l’ultima a utilizzare il dato. Ma ci sono vari scenari in questa possibilità può risultare utile. La sua semplicità, la gestione automatica, della memoria e della condivisione sono tra i punti di forza che fanno di Rc uno strumento molto utile e pare assai gradito a chi viene da linguaggio garbage collected (che a volte tende ad abusarne...). Vediamo subito l'esempio che introduce l'argomento:
La riga 1 ci presenta una struttura, ovvero la protagonista di questo paragrafo Rc definita internamente come segue: pub struct Rc<T, A = Global> where A: Allocator, T: ?Sized, { /* private fields */ } La definizione in sè non è semplicissima e va un po' oltre le nostre conoscenze a questo punto ma proviamo comunque a darne una spiegazione: Rc sta per appunto per "Reference Counted" (contato per riferimento) ed è un tipo di smart pointer che consente di condividere la proprietà di un valore tra più parti del programma. Utilizza il conteggio dei riferimenti per tenere traccia di quante volte il valore è stato referenziato, e dealloca la memoria quando il conteggio dei riferimenti scende a zero. pub: La struttura è pubblica e può essere utilizzata al di fuori del modulo in cui è definita. struct Rc<T, A = Global>: Definisce una struttura generica Rc con due parametri di tipo: T: Il tipo del valore che Rc contiene. A = Global: Il tipo dell'allocatore utilizzato per gestire la memoria. Di default, è Global, che è l'allocatore predefinito di Rust. where A: Allocator, T: ?Sized: where A: Allocator: Specifica, come sappiamo, che A deve implementare il trait Allocator. Questo significa che A deve essere in grado di gestire l'allocazione e la deallocazione della memoria. T: ?Sized: Indica che T può essere un tipo di dimensione sconosciuta (?Sized). Questo permette a Rc di contenere tipi di dimensione fissa (come i32) o tipi di dimensione dinamica (come str o dyn Trait). { /* private fields */ }: I campi della struttura sono privati e non accessibili direttamente dall'esterno. Questo è comune per garantire l'incapsulamento e la sicurezza dei dati. Ed ecco l'output del codice precedente:
notiamo subito come i riferimenti al valore siano in effetti 2. Ovviamente strong_count è il metodo che ci restituisce il numero di puntatori all'elemento interessato mentre clone crea un ulteriore puntatore. Ecco, il punto da ricordare è che non viene effettuata alcuna copia c'è solo un incremento di puntatori verso la zona di memoria che contiene l'elemento, in questo caso il numero 42. Come abbiamo detto Rc è uno smart pointer quindi non possiamo usare direttamente le variabili val_cond o val_cond_clone ad esempio per calcoli aritmetici. E' necessario, come ormai saprete dal capitolo relativo ai puntatori, ricorrere ad una dereferenziazione attraverso l'operatore *. Il codice è molto semplice:
use std::rc::Rc;
fn main() {
let refc = Rc::new(42);
let r1 = *refc;
println!("{}", r1 / 2);
}
con r1 che contiene il valore effettivamente allocato laddove punta il riferimento refc. Un'altra domanda che potremmo farci è come eliminare un riferimento. In questo caso si ricorre all'uso esplicito di drop, funzione che internamente è definita in modo molto lineare: pub fn drop<T> quindi riprendendo l'esempio 1:
e in questo caso avremo:
L'altro modo di eliminare un riferimento evidentemente è quello di farlo uscire dal suo ambito. Rc<T> ha anche un vantaggio che lo rende molto popolare. Pensiamoci un attimo: tiene conto dei riferimenti e del numero di quelli in essere... ma allora è molto vicino (non così "molto" in realtà...) al complesso concetto di Lifetime! Come abbiamo visto, in Rust, i lifetimes sono usati per assicurarsi che i riferimenti a dei dati siano validi per l'intera durata in cui vengono utilizzati. Quando si scrivono funzioni che restituiscono riferimenti, o si passa tra strutture dei riferimenti a dati, il compilatore richiede di specificare i lifetimes per garantire la sicurezza della memoria. L'uso di Rc<T> introduce un diverso modo di gestire la durata del dato. Poiché Rc è un tipo di smart pointer che tiene traccia del numero di riferimenti attivi a un dato, esso garantisce automaticamente che il dato esista finché c'è almeno un riferimento attivo. Mi pare che Python adotti un meccanismo simile basato sul conteggio dei riferimenti, per fare un esempio "nobile". Quando l'ultimo riferimento a un Rc viene rimosso, il dato viene deallocato automaticamente. Questo dovrebbe essere chiaro a questo punto. Vediamo come Rc può sostituirsi al lifetime analizzando un caso tipico:
Questo classico esempio si può riscrivere come segue usando le nuove possibilità che abbiamo incontrato in questo paragrafo:
Quanto detto finora però non deve far pensare ad una reale intercambiabilità tra il meccanismo di lifetime e Rc. In realtà essi riferiscono ad ambiti del tutto diversi se ci riflettiamo un attimo. Rc<T> serve per la condivisione di risorse o meglio per la condivisione dell'ownership di risorse. Invece e annotazioni di lifetime servono a indicare al compilatore quanto a lungo i riferimenti a un dato devono rimanere validi, fornendo al compilatore una sorta di mappa del tesoro che indica cosa controllare. Non hanno a che fare con l'ownership, ma servono a garantire che i riferimenti non diventino mai dangling references (riferimenti non validi). Il loro scenario di utilizzo è la gestione dei riferimenti a dati che non possiedi direttamente, e vuoi assicurarti che non accada un use-after-free (uso di memoria deallocata). Va poi considerato che Rc<T> di per sè stesso non gestisce l'ambito multitrheading e nemmeno la mutabilità, (ovvero una variabile creata con Rc non può essere modificata nel suo valore), bisogna ricorrere come vedremo tra poco, a Avrete intuito che Rc<T> introduce un rischio, ovvero quello di creare dei riferimenti circolari. Esiste allora la possibilità usare Weak<T> che crea un riferimento senza incrementare il numero di puntamenti. Vedremo questa possibilità dopo aver parlato di RefCell. Molto simile ad Rc<T> è Arc<T> che però funziona in ambito multi-thread. Il principio è lo stesso, ovvero gestire più riferimenti ma funziona in ambiente multithread. Come questo avvenga lo chiarisce, in parte la sua stessa definizione: Arc sta per Atomic Rc. Questo, detto in modo semplice, significa che vengono adoperate operazioni "atomiche" o meglio indivisibili, ininterrotte di modo che non vi sia alcun thread che possa interromperne o modificarne l'esecuzione. Ovviamente c'è un prezzo da pagare: l'uso di operazione atomiche non è privo di costi in termini prestazionali. Il funzionamento è del tutto simile ad Rc<T>, per esempio potremmo riscrivere l'esempo 41.1 come segue:
use std::sync::Arc;
fn main() {
let val = Arc::new(42);
let val_clone = Arc::clone(&val);
println!("valore condiviso: {}", val);
println!("valore clone: {}", val_clone);
println!("Conteggio riferimenti: {}",
Arc::strong_count(&val));
}
ma sarebbe del tutto inutile in assenza di ambito con più thread. Vediamo invece un esempio più appropriato per Arc<T> esempio che anticipa qualche concetto che vedremo più avanti:
Se provate ad utilizzare Rc<t> invece di Arc<T>, la sostituzione è facile, vedrete che il compilatore se la prende molto male. |