L'un des principaux objectifs du projet Rust consiste à améliorer les données relatives à la concurrence, de sorte que des améliorations sont à prévoir, à condition que le projet atteigne ses objectifs. Clause de non-responsabilité: J'ai une haute opinion de Rust et je suis investie dans celle-ci. Comme demandé, je vais essayer d'éviter les jugements de valeur et décrire les différences plutôt que les améliorations (IMHO) .
Rouille sûre et peu sûre
"Rust" est composé de deux langages: un qui s'efforce de vous isoler des dangers de la programmation système et un autre plus puissant sans ces aspirations.
Unsafe Rust est une langue méchante et brutale qui ressemble beaucoup au C ++. Il vous permet de faire des choses arbitrairement dangereuses, de parler au matériel, de (mal) gérer manuellement la mémoire, de vous tirer une balle dans le pied, etc. et les mains de tous les autres programmeurs impliqués. Vous choisissez cette langue avec le mot clé unsafe
. Comme en C et C ++, une seule erreur à un seul endroit peut entraîner la défaillance totale du projet.
Safe Rust est le "défaut", la grande majorité du code Rust est sécurisée, et si vous ne mentionnez jamais le mot clé unsafe
dans votre code, vous ne quittez jamais la langue sécurisée. Le reste du poste sera principalement consacré à cette langue, car le unsafe
code peut casser toute garantie que la sécurité de Rust fonctionne si durement pour vous. D'un autre côté, le unsafe
code n'est pas mauvais et n'est pas traité comme tel par la communauté (il est toutefois fortement découragé lorsqu'il n'est pas nécessaire).
C'est dangereux, certes, mais aussi important, car cela permet de construire les abstractions utilisées par le code sécurisé. Un bon code non sécurisé utilise le système de typage pour empêcher les autres d’en abuser. Par conséquent, la présence de code non sécurisé dans un programme Rust ne doit pas perturber le code sécurisé. Toutes les différences suivantes existent, car les systèmes de types de Rust ont des outils que C ++ ne possède pas et parce que le code non sécurisé qui implémente les abstractions de concurrence utilise ces outils efficacement.
Non-différence: mémoire partagée / mutable
Bien que Rust mette davantage l'accent sur le passage de messages et contrôle très strictement la mémoire partagée, il n'exclut pas la concurrence de la mémoire partagée et prend en charge explicitement les abstractions communes (verrous, opérations atomiques, variables de condition, collections simultanées).
De plus, comme C ++ et contrairement aux langages fonctionnels, Rust aime vraiment les structures de données impératives traditionnelles. Il n'y a pas de liste chaînée persistante / immuable dans la bibliothèque standard. Il y a std::collections::LinkedList
mais c'est comme std::list
en C ++ et découragé pour les mêmes raisons que std::list
(mauvaise utilisation du cache).
Cependant, en ce qui concerne le titre de cette section ("mémoire partagée / mutable"), Rust a une différence par rapport à C ++: il encourage vivement à ce que la mémoire soit "partagée XOR mutable", c’est-à-dire qu’elle ne soit jamais partagée et ne peut pas être mutée en même temps. temps. Mutatez la mémoire à votre guise "dans l'intimité de votre propre fil", pour ainsi dire. Comparez cela avec C ++ où la mémoire mutable partagée est l'option par défaut et largement utilisée.
Bien que le paradigme de xor-mutable partagé soit très important pour les différences ci-dessous, il s’agit également d’un paradigme de programmation tout à fait différent, qui prend un certain temps à s’habituer et impose des restrictions importantes. De temps en temps, il faut sortir de ce paradigme, par exemple avec des types atomiques ( AtomicUsize
est l'essence de la mémoire mutable partagée). Notez que les verrous obéissent également à la règle shared-xor-mutable, car ils excluent les lectures et les écritures simultanées (pendant qu'un thread écrit, aucun autre thread ne peut lire ou écrire).
Non-différence: les courses de données sont un comportement indéfini (UB)
Si vous déclenchez une course de données en code Rust, la partie est terminée, tout comme en C ++. Tous les paris sont ouverts et le compilateur peut faire ce qu'il veut.
Cependant, il est une garantie absolue que le code Rust en toute sécurité ne comporte pas de courses de données (ni d’UB pour l’instant). Cela s'étend à la fois au langage principal et à la bibliothèque standard. Si vous pouvez écrire un programme Rust qui n'utilise pas unsafe
(y compris dans des bibliothèques tierces mais excluant la bibliothèque standard) ce qui déclenche UB, cela est considéré comme un bogue et sera corrigé (cela s'est déjà produit plusieurs fois). Ceci, bien sûr, contraste avec le C ++, où écrire des programmes avec UB est une tâche triviale.
Différence: discipline de verrouillage stricte
Contrairement à C ++, une serrure à Rust ( std::sync::Mutex
, std::sync::RwLock
, etc.) possède les données qu'il est protection. Au lieu de verrouiller et de manipuler une mémoire partagée associée au verrou uniquement dans la documentation, les données partagées sont inaccessibles tant que vous ne maintenez pas le verrou. Un garde RAII garde le verrou et donne simultanément accès aux données verrouillées (ceci pourrait être mis en œuvre par C ++, mais pas par les std::
verrous). Le système de durée de vie garantit que vous ne pouvez pas continuer à accéder aux données après avoir relâché le verrou (laissez tomber la protection RAII).
Vous pouvez bien sûr avoir un verrou qui ne contient aucune donnée utile ( Mutex<()>
) et simplement partager de la mémoire sans l'associer explicitement à ce verrou. Cependant, avoir de la mémoire partagée potentiellement non synchronisée nécessite unsafe
.
Différence: prévention du partage accidentel
Bien que vous puissiez avoir la mémoire partagée, vous ne partagez que lorsque vous le demandez explicitement. Par exemple, lorsque vous utilisez la transmission de messages (par exemple, les canaux de std::sync
), le système de durée de vie garantit que vous ne gardez aucune référence aux données après les avoir envoyées à un autre thread. Pour partager des données derrière un verrou, vous construisez explicitement le verrou et vous le donnez à un autre thread. Pour partager la mémoire non synchronisée avec unsafe
vous, bien, vous devez utiliser unsafe
.
Cela rejoint le point suivant:
Différence: suivi de la sécurité des fils
Le système de typage de Rust suit certaines notions de sécurité des threads. Spécifiquement, le Sync
trait désigne les types qui peuvent être partagés par plusieurs threads sans risque de course des données, alors que Send
ceux qui peuvent être déplacés d'un thread à un autre. Ceci est appliqué par le compilateur tout au long du programme et les concepteurs de bibliothèques osent donc faire des optimisations qui seraient bêtement dangereuses sans ces contrôles statiques. Par exemple, C ++ std::shared_ptr
qui utilise toujours des opérations atomiques pour manipuler son décompte de références, afin d'éviter UB s'il shared_ptr
est utilisé par plusieurs threads. Rust a Rc
et Arc
qui ne diffèrent que par les Rc
utilisations des opérations de Refcount non atomiques et n'est pas threadsafe (c. -à ne pas mettre en œuvre Sync
ou Send
) tout Arc
est très semblable àshared_ptr
(et implémente les deux traits).
Notez que si un type ne pas utiliser unsafe
pour mettre en œuvre manuellement la synchronisation, la présence ou l' absence des traits sont inférées correctement.
Différence: règles très strictes
Si le compilateur ne peut pas être absolument sûr que certains codes sont exempts de données et d'autres UB, il ne compilera pas, un point c'est tout . Les règles mentionnées ci-dessus et d'autres outils peuvent vous mener assez loin, mais tôt ou tard, vous voudrez faire quelque chose de correct, mais pour des raisons subtiles qui échappent à l'avis du compilateur. Cela pourrait être une structure de données sans verrouillage délicate, mais cela pourrait être aussi banal que "j'écris à des emplacements aléatoires dans un tableau partagé, mais les index sont calculés de telle sorte que chaque emplacement est écrit par un seul thread".
À ce stade, vous pouvez soit mordre la balle et ajouter un peu de synchronisation inutile, soit vous reformulez le code de sorte que le compilateur puisse en voir l'exactitude (souvent faisable, parfois assez difficile, parfois impossible) ou passer au unsafe
code. Néanmoins, il s’agit d’une charge mentale supplémentaire et Rust ne vous donne aucune garantie quant à l’exactitude du unsafe
code.
Différence: moins d'outils
En raison des différences susmentionnées, il est beaucoup plus rare à Rust d'écrire du code pouvant avoir une course de données (ou une utilisation après libre, ou un double libre, ou ...). Bien que ce soit une bonne chose, le fait que l’écosystème permettant de détecter de telles erreurs soit encore plus sous-développé que l’on pourrait s’attendre compte tenu de la jeunesse et de la petite taille de la communauté a malheureusement un effet secondaire.
Des outils tels que valgrind et le désinfectant de fil de LLVM pourraient en principe être appliqués au code Rust, que cela fonctionne ou non varie (d’un outil à l’autre) (et même ceux qui fonctionnent peuvent être difficiles à configurer, d’autant plus que -date ressources sur la façon de le faire). Cela n'aide vraiment pas que Rust manque actuellement d'une spécification réelle et en particulier d'un modèle de mémoire formel.
En bref, écrire unsafe
correctement le code Rust est plus difficile que d’écrire correctement le code C ++, bien que les deux langages soient à peu près comparables en termes de capacités et de risques. Bien sûr, cela doit être mis en balance avec le fait qu'un programme Rust typique ne contiendra qu'une fraction relativement petite du unsafe
code, alors qu'un programme C ++ est, enfin, entièrement en C ++.