Una migliore storia della concorrenza è uno degli obiettivi principali del progetto Rust, quindi dovrebbero essere previsti miglioramenti, a condizione che ci fidiamo del progetto per raggiungere i suoi obiettivi. Disclaimer completo: ho un'alta opinione di Rust e ne sono investito. Come richiesto, cercherò di evitare giudizi di valore e descriverò le differenze piuttosto che i miglioramenti (IMHO) .
Ruggine sicura e non sicura
"Rust" è composto da due lingue: una che fa di tutto per isolarti dai pericoli della programmazione dei sistemi, e una più potente senza tali aspirazioni.
Unsafe Rust è un linguaggio brutto e brutale che assomiglia molto al C ++. Ti permette di fare cose arbitrariamente pericolose, parlare con l'hardware, (mis-) gestire la memoria manualmente, spararti ai piedi, ecc. È molto simile a C e C ++ in quanto la correttezza del programma è in definitiva nelle tue mani e le mani di tutti gli altri programmatori coinvolti. Si sceglie questo linguaggio con la parola chiave unsafe
e, come in C e C ++, un singolo errore in una singola posizione può far crollare l'intero progetto.
Safe Rust è il "valore predefinito", la stragrande maggioranza del codice Rust è sicura e se non menzioni mai la parola chiave unsafe
nel tuo codice, non lasci mai la lingua sicura. Il resto del post si occuperà principalmente di quella lingua, perché il unsafe
codice può infrangere qualsiasi e tutte le garanzie che Rust Rust lavora così duramente per darti. D'altro canto, il unsafe
codice non è malvagio e non viene trattato come tale dalla comunità (è, tuttavia, fortemente scoraggiato quando non necessario).
È pericoloso, sì, ma anche importante, perché consente di costruire le astrazioni che utilizza il codice sicuro. Un buon codice non sicuro utilizza il sistema dei tipi per impedire ad altri di utilizzarlo in modo improprio, pertanto la presenza di un codice non sicuro in un programma Rust non deve disturbare il codice sicuro. Esistono tutte le seguenti differenze perché i sistemi di tipi di Rust hanno strumenti che C ++ non possiede e perché il codice non sicuro che implementa le astrazioni di concorrenza utilizza questi strumenti in modo efficace.
Non-differenza: memoria condivisa / modificabile
Sebbene Rust ponga maggiormente l'accento sul passaggio dei messaggi e controlli molto rigorosamente la memoria condivisa, non esclude la concorrenza della memoria condivisa e supporta esplicitamente le astrazioni comuni (blocchi, operazioni atomiche, variabili di condizione, raccolte simultanee).
Inoltre, come il C ++ e, diversamente dai linguaggi funzionali, Rust ama davvero le strutture di dati imperativi tradizionali. Non esiste un elenco di collegamenti persistenti / immutabili nella libreria standard. C'è std::collections::LinkedList
ma è come std::list
in C ++ e scoraggiato per gli stessi motivi di std::list
(cattivo uso della cache).
Tuttavia, con riferimento al titolo di questa sezione ("memoria condivisa / mutabile"), Rust ha una differenza rispetto al C ++: incoraggia fortemente che la memoria sia "condivisa XOR mutabile", cioè che la memoria non sia mai condivisa e mutevole allo stesso modo tempo. Muta la memoria come preferisci "nella privacy del tuo thread", per così dire. Contrastalo con C ++, dove la memoria mutabile condivisa è l'opzione predefinita e ampiamente usata.
Mentre il paradigma condiviso-xor-mutabile è molto importante per le differenze sottostanti, è anche un paradigma di programmazione abbastanza diverso che richiede un po 'di tempo per abituarsi e che pone restrizioni significative. Occasionalmente si deve rinunciare a questo paradigma, ad esempio, con tipi atomici ( AtomicUsize
è l'essenza della memoria mutabile condivisa). Nota che i lock obbediscono anche alla regola condivisa-xor-mutable, perché esclude letture e scritture simultanee (mentre un thread scrive, nessun altro thread può leggere o scrivere).
Non-differenza: le gare di dati sono comportamenti indefiniti (UB)
Se attivi una corsa di dati nel codice Rust, il gioco è finito, proprio come in C ++. Tutte le scommesse sono disattivate e il compilatore può fare quello che vuole.
Tuttavia, è una garanzia concreta che il codice Rust sicuro non abbia gare di dati (o UB per quella materia). Questo si estende sia al linguaggio principale che alla libreria standard. Se riesci a scrivere un programma Rust che non utilizza unsafe
(incluso nelle librerie di terze parti ma escludendo la libreria standard) che attiva UB, viene considerato un bug e verrà corretto (ciò è già accaduto più volte). Questo ovviamente è in netto contrasto con C ++, dove è banale scrivere programmi con UB.
Differenza: rigorosa disciplina di blocco
A differenza di C ++, una serratura a Rust ( std::sync::Mutex
, std::sync::RwLock
, etc.) possiede i dati che sta proteggendo. Invece di prendere un blocco e quindi manipolare parte della memoria condivisa associata al blocco solo nella documentazione, i dati condivisi sono inaccessibili mentre non si tiene il blocco. Una guardia RAII mantiene il blocco e contemporaneamente dà accesso ai dati bloccati (questo potrebbe essere implementato da C ++, ma non dai std::
blocchi). Il sistema a vita garantisce che non è possibile continuare ad accedere ai dati dopo aver rilasciato il blocco (rilasciare la protezione RAII).
Ovviamente puoi avere un blocco che non contiene dati utili ( Mutex<()>
) e condividere solo un po 'di memoria senza associarlo esplicitamente a quel blocco. Tuttavia, è necessaria una memoria condivisa potenzialmente non sincronizzata unsafe
.
Differenza: prevenzione della condivisione accidentale
Anche se puoi avere una memoria condivisa, condividi solo quando la chiedi esplicitamente. Ad esempio, quando si utilizza il passaggio di messaggi (ad es. I canali da std::sync
), il sistema a vita garantisce di non conservare alcun riferimento ai dati dopo averli inviati a un altro thread. Per condividere i dati dietro un blocco, costruisci esplicitamente il blocco e lo dai a un altro thread. Per condividere la memoria non sincronizzata con unsafe
te, bene, devi usare unsafe
.
Questo si lega al punto successivo:
Differenza: monitoraggio della sicurezza del filo
Il sistema di tipo Rust tiene traccia di alcune nozioni di sicurezza del thread. In particolare, il Sync
tratto indica i tipi che possono essere condivisi da più thread senza rischio di corse di dati, mentre Send
segna quelli che possono essere spostati da un thread a un altro. Ciò è imposto dal compilatore in tutto il programma, e quindi i progettisti di librerie osano fare ottimizzazioni che sarebbero stupidamente pericolose senza questi controlli statici. Ad esempio, quelli di C ++ std::shared_ptr
che usano sempre operazioni atomiche per manipolare il conteggio dei riferimenti, per evitare UB se a shared_ptr
capita di essere usato da più thread. Rust ha Rc
e Arc
, che differiscono solo per il fatto che Rc
utilizza operazioni di conteggio non atomico e non è thread-safe (ovvero non implementa Sync
o Send
) mentre Arc
è molto simile ashared_ptr
(e implementa entrambi i tratti).
Si noti che se un tipo non viene utilizzato unsafe
per implementare manualmente la sincronizzazione, la presenza o l'assenza dei tratti viene dedotta correttamente.
Differenza: regole molto rigide
Se il compilatore non può essere assolutamente sicuro che un codice sia privo di corse di dati e altri UB, non verrà compilato, punto . Le regole di cui sopra e altri strumenti possono portarti abbastanza lontano, ma prima o poi vorrai fare qualcosa di corretto, ma per ragioni sottili che sfuggono all'avviso del compilatore. Potrebbe essere una complessa struttura di dati priva di blocchi, ma potrebbe anche essere qualcosa di banale come "scrivo in posizioni casuali in un array condiviso ma gli indici sono calcolati in modo tale che ogni posizione sia scritta da un solo thread".
A quel punto puoi o mordere il proiettile e aggiungere un po 'di sincronizzazione non necessaria, oppure riformulare il codice in modo tale che il compilatore possa vederne la correttezza (spesso fattibile, a volte piuttosto difficile, a volte impossibile), oppure passi al unsafe
codice. Tuttavia, è un sovraccarico mentale extra e Rust non ti dà alcuna garanzia per la correttezza del unsafe
codice.
Differenza: meno strumenti
A causa delle differenze di cui sopra, in Rust è molto più raro che si scriva codice che può avere una corsa di dati (o un uso dopo libero, o un doppio libero, o ...). Anche se questo è bello, ha lo sfortunato effetto collaterale che l'ecosistema per rintracciare tali errori è ancora più sottosviluppato di quanto ci si aspetterebbe, data la giovinezza e le dimensioni ridotte della comunità.
Mentre strumenti come valgrind e il disinfettante per thread di LLVM potrebbero in linea di principio essere applicati al codice Rust, se questo in realtà funziona ancora varia da strumento a strumento (e anche quelli che lavorano potrebbero essere difficili da configurare, soprattutto perché potresti non trovare alcun aggiornamento -date risorse su come farlo). In realtà non aiuta che Rust manchi di specifiche reali e in particolare di un modello di memoria formale.
In breve, scrivere unsafe
correttamente il codice Rust è più difficile che scrivere correttamente il codice C ++, nonostante entrambe le lingue siano approssimativamente comparabili in termini di capacità e rischi. Naturalmente questo deve essere ponderato rispetto al fatto che un tipico programma Rust conterrà solo una frazione relativamente piccola di unsafe
codice, mentre un programma C ++ è, beh, completamente C ++.