Comment Rust diffère-t-il des fonctionnalités de simultanéité de C ++?


35

Des questions

J'essaie de comprendre si Rust améliore fondamentalement et suffisamment les fonctionnalités de concurrence du C ++ pour pouvoir décider si je devrais passer le temps nécessaire pour apprendre Rust.

En particulier, comment la rouille idiomatique s’améliore-t-elle, ou du moins s’écarte-t-elle, des fonctionnalités de concurrence du C ++ idiomatique?

L'amélioration (ou la divergence) est-elle principalement syntaxique ou s'agit-il essentiellement d'une amélioration (divergence) du paradigme? Ou s'agit-il d'autre chose? Ou n'est-ce pas vraiment une amélioration (divergence)?


Raisonnement

J'ai récemment essayé d'apprendre moi-même les facilités d'accès simultané de C ++ 14, et quelque chose ne semble pas très bien. Quelque chose ne va pas. Qu'est-ce qui se sent? Dur à dire.

C'est presque comme si le compilateur n'essayait pas vraiment de m'aider à écrire des programmes corrects en matière de simultanéité. C'est comme si j'utilisais un assembleur plutôt qu'un compilateur.

Certes, il est tout à fait probable que je souffre encore d’un concept subtil et défectueux en matière de concurrence. Peut-être que je ne ressens pas encore la tension de Bartosz Milewski entre la programmation avec état et les courses de données. Peut-être que je ne comprends pas tout à fait la quantité de méthodologie concurrente sonore dans le compilateur et dans le système d’exploitation.

Réponses:


56

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é unsafedans votre code, vous ne quittez jamais la langue sécurisée. Le reste du poste sera principalement consacré à cette langue, car le unsafecode peut casser toute garantie que la sécurité de Rust fonctionne si durement pour vous. D'un autre côté, le unsafecode 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::LinkedListmais c'est comme std::listen 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 ( AtomicUsizeest 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 unsafevous, 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 Synctrait désigne les types qui peuvent être partagés par plusieurs threads sans risque de course des données, alors que Sendceux 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_ptrqui utilise toujours des opérations atomiques pour manipuler son décompte de références, afin d'éviter UB s'il shared_ptrest utilisé par plusieurs threads. Rust a Rcet Arcqui 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 Syncou Send) tout Arcest très semblable àshared_ptr (et implémente les deux traits).

Notez que si un type ne pas utiliser unsafepour 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 unsafecode. Néanmoins, il s’agit d’une charge mentale supplémentaire et Rust ne vous donne aucune garantie quant à l’exactitude du unsafecode.

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 unsafecorrectement 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 unsafecode, alors qu'un programme C ++ est, enfin, entièrement en C ++.


6
Où sur mon écran se trouve le commutateur +25 votes positifs? Je ne peux pas le trouver! Cette réponse informative est très appréciée. Cela ne me laisse aucune question évidente sur les points abordés. Donc, à d’autres points: si je comprends la documentation de Rust, Rust a [a] des installations de test intégrées et [b] un système de construction appelé Cargo. Sont-ils raisonnablement prêts pour la production à votre avis? De plus, en ce qui concerne Cargo, est-il humoristique de me laisser ajouter des scripts shell, Python et Perl, la compilation LaTeX, etc., au processus de construction?
thb

2
@thb Le matériel de test est très simple (par exemple, pas moqueur) mais fonctionnel. Le fret fonctionne assez bien, bien que l'accent mis sur Rust et sur la reproductibilité signifie que ce n'est peut-être pas la meilleure option pour couvrir toutes les étapes, du code source aux artefacts finaux. Vous pouvez écrire des scripts de construction, mais cela peut ne pas être approprié pour tout ce que vous mentionnez. (Cependant, les gens utilisent régulièrement des scripts de compilation pour compiler les bibliothèques C ou trouver des versions existantes de bibliothèques C, de sorte que Cargo cesse de fonctionner lorsque vous utilisez plus que de la pure Rust.)

2
En passant, pour ce que ça vaut, votre réponse est assez concluante. Depuis que j'aime le C ++, puisque celui-ci dispose d'installations décentes pour presque tout ce que je devais faire, puisque le C ++ est stable et largement utilisé, je suis jusqu'à présent assez satisfait d'utiliser le C ++ pour toutes les utilisations possibles légères (je n'ai jamais développé d'intérêt pour Java , par exemple). Mais maintenant nous avons la concurrence, et C ++ 14 me semble lutter avec. Je n'ai pas essayé volontairement un nouveau langage de programmation depuis une décennie, mais (à moins que Haskell n'apparaisse comme une meilleure option), je pense que je vais devoir essayer 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.En fait, cela reste le cas même avec des unsafeéléments. Les pointeurs simplement bruts ne sont pas non Syncplus Sharece qui signifie que par défaut, les structures qui les contiennent n'auront ni l'un ni l'autre.
Hauleth

@ ŁukaszNiemier Il peut arriver que les choses se passent bien, mais il y a un milliard de façons dont un type à utilisation non sécurisée peut aboutir Sendou Syncmême s'il ne le devrait vraiment pas.

-2

La rouille ressemble aussi beaucoup à Erlang et à Go. Il communique à l'aide de canaux dotés de mémoires tampons et d'une attente conditionnelle. Tout comme Go, il assouplit les restrictions d'Erlang en vous permettant de faire de la mémoire partagée, de prendre en charge le comptage de références atomiques et les verrous, et en vous permettant de passer des canaux d'un thread à l'autre.

Cependant, Rust va encore plus loin. Alors que Go vous fait confiance pour faire ce qui est bien, Rust assigne un mentor qui vous assiste et se plaint si vous essayez de faire ce qui est mal. Le mentor de Rust est le compilateur. Il effectue une analyse sophistiquée pour déterminer la propriété des valeurs transmises aux threads et génère des erreurs de compilation en cas de problèmes potentiels.

Vous trouverez ci-dessous une citation de la documentation RUST.

Les règles de propriété jouent un rôle essentiel dans l'envoi de messages, car elles nous aident à écrire du code sécurisé, simultané. La prévention des erreurs dans la programmation simultanée est l’avantage que nous obtenons en faisant le compromis de devoir penser à la propriété tout au long de nos programmes Rust. - Message passant avec propriété des valeurs.

Si Erlang est draconien et que Go est un État libre, alors Rust est un État nourrice.

Vous pouvez trouver plus d'informations dans les idéologies de concurrence des langages de programmation: Java, C #, C, C +, Go et Rust.


2
Bienvenue sur Stack Exchange! Veuillez noter que chaque fois que vous créez un lien vers votre propre blog, vous devez l'indiquer explicitement. voir le centre d'aide .
Glorfindel
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.