In che modo Rust differisce dalle strutture di concorrenza di C ++?


35

Domande

Sto cercando di capire se Rust migliora fondamentalmente e sufficientemente le strutture di concorrenza del C ++ in modo da decidere se dovrei spendere il tempo per imparare Rust.

In particolare, in che modo la ruggine idiomatica migliora o comunque differisce dalle strutture di concorrenza del C ++ idiomatico?

Il miglioramento (o la divergenza) è per lo più sintattico o è sostanzialmente un miglioramento (divergenza) nel paradigma? O è qualcos'altro? O non è davvero un miglioramento (divergenza)?


Fondamento logico

Di recente ho cercato di insegnare a me stesso le strutture di concorrenza di C ++ 14 e qualcosa non sembra del tutto giusto. Qualcosa si sente fuori. Cosa ci si sente? Difficile da dire.

Sembra quasi che il compilatore non stia davvero cercando di aiutarmi a scrivere programmi corretti quando si tratta di concorrenza. Mi sembra quasi di usare un assemblatore piuttosto che un compilatore.

Certo, è del tutto probabile che io soffra ancora di un concetto sottile e difettoso quando si tratta di concorrenza. Forse non ho ancora capito la tensione di Bartosz Milewski tra programmazione con stato e gare di dati. Forse non capisco bene quanto della metodologia simultanea del suono sia nel compilatore e quanto nel sistema operativo.

Risposte:


56

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 unsafee, 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 unsafenel tuo codice, non lasci mai la lingua sicura. Il resto del post si occuperà principalmente di quella lingua, perché il unsafecodice può infrangere qualsiasi e tutte le garanzie che Rust Rust lavora così duramente per darti. D'altro canto, il unsafecodice 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::LinkedListma è come std::listin 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 unsafete, 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 Synctratto indica i tipi che possono essere condivisi da più thread senza rischio di corse di dati, mentre Sendsegna 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_ptrche usano sempre operazioni atomiche per manipolare il conteggio dei riferimenti, per evitare UB se a shared_ptrcapita di essere usato da più thread. Rust ha Rce Arc, che differiscono solo per il fatto che Rc utilizza operazioni di conteggio non atomico e non è thread-safe (ovvero non implementa Synco Send) mentre Arcè molto simile ashared_ptr (e implementa entrambi i tratti).

Si noti che se un tipo non viene utilizzato unsafeper 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 unsafecodice. Tuttavia, è un sovraccarico mentale extra e Rust non ti dà alcuna garanzia per la correttezza del unsafecodice.

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 unsafecorrettamente 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 unsafecodice, mentre un programma C ++ è, beh, completamente C ++.


6
Dove si trova il mio interruttore +25 upvote sul mio schermo? Non riesco a trovarlo! Questa risposta informativa è molto apprezzata. Non mi lascia domande ovvie sui punti che copre. Quindi, ad altri punti: se capisco la documentazione di Rust, Rust ha [a] strutture di test integrate e [b] un sistema di costruzione chiamato Cargo. Questi sono ragionevolmente pronti per la produzione secondo te? Inoltre, per quanto riguarda Cargo, è di buon umore lasciarmi aggiungere shell, script Python e Perl, compilazione LaTeX, ecc., Al processo di compilazione?
THB

2
@thb Il materiale di prova è molto semplice (ad es. non beffardo) ma funzionale. Il carico funziona abbastanza bene, anche se la sua attenzione su Rust e sulla riproducibilità significa che potrebbe non essere l'opzione migliore per coprire tutti i passaggi dal codice sorgente agli artefatti finali. Puoi scrivere script di compilazione, ma potrebbe non essere appropriato per tutte le cose che menzioni. (Le persone, tuttavia, usano regolarmente gli script di compilazione per compilare librerie C o trovare versioni esistenti di librerie C, quindi non è come se Cargo

2
A proposito, per quello che vale, la tua risposta sembra piuttosto conclusiva. Dal momento che mi piace il C ++, poiché il C ++ ha strutture decenti per quasi tutto ciò che ho bisogno di fare, dal momento che il C ++ è stabile e ampiamente utilizzato, finora sono stato abbastanza soddisfatto di usare il C ++ per ogni possibile scopo non leggero (non ho mai sviluppato un interesse per Java , per esempio). Ma ora abbiamo la concorrenza e C ++ 14 mi sembra alle prese con esso. Non ho volontariamente provato un nuovo linguaggio di programmazione in un decennio, ma (a meno che Haskell non appaia un'opzione migliore) penso che dovrò provare Rust.
THB

Note that if a type doesn't use unsafe to manually implement synchronization, the presence or absence of the traits are inferred correctly.in realtà funziona ancora anche con unsafeelementi. Solo i puntatori non Syncelaborati non sono né il Shareche significa che per impostazione predefinita struct non li avrà.
Hauleth,

@ ŁukaszNiemier Può succedere che vada bene, ma ci sono miliardi di modi in cui un tipo non sicuro può finire Sendo Syncanche se non dovrebbe davvero.

-2

La ruggine è anche molto simile a Erlang and Go. Comunica utilizzando canali con buffer e attesa condizionale. Proprio come Go, rilassa le restrizioni di Erlang permettendoti di fare memoria condivisa, supportare il conteggio dei riferimenti atomici e i blocchi e lasciandoti passare i canali da thread a thread.

Tuttavia, Rust fa un ulteriore passo avanti. Mentre Go si fida di fare la cosa giusta, Rust assegna un mentore che si siede con te e si lamenta se provi a fare la cosa sbagliata. Il mentore di Rust è il compilatore. Esegue analisi sofisticate per determinare la proprietà dei valori passati ai thread e fornire errori di compilazione in caso di potenziali problemi.

Di seguito è riportata una citazione dai documenti RUST.

Le regole di proprietà svolgono un ruolo vitale nell'invio di messaggi perché ci aiutano a scrivere codice simultaneo e sicuro. Prevenire gli errori nella programmazione concorrente è il vantaggio che otteniamo facendo il compromesso di dover pensare alla proprietà durante i nostri programmi Rust. - Messaggio che passa con la proprietà dei valori.

Se Erlang è draconiano e Go è uno stato libero, Rust è uno stato di babysitter.

Puoi trovare ulteriori informazioni dalle ideologie di concorrenza dei linguaggi di programmazione: Java, C #, C, C +, Go e Rust


2
Benvenuto in Stack Exchange! Si prega di notare che ogni volta che si collega al proprio blog, è necessario dichiararlo in modo esplicito; consultare il centro assistenza .
Glorfindel,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.