web-dev-qa-db-fra.com

Comment Rust s'écarte-t-il des fonctionnalités de concurrence du C ++?

Des questions

J'essaie de comprendre si Rust améliore fondamentalement et suffisamment les fonctionnalités de simultanéité de C++ afin de décider si je dois passer du temps à apprendre Rust.

Plus précisément, comment idiomatic Rust améliore-t-il, ou en tout cas diffère-t-il 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é de m'enseigner moi-même les fonctionnalités de concurrence de C++ 14, et quelque chose ne me semble pas tout à fait correct. Quelque chose se sent mal. Quoi se sent mal? Dur à dire.

J'ai l'impression que le compilateur n'essayait pas vraiment de m'aider à écrire des programmes corrects en ce qui concerne la concurrence. C'est presque 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 dynamique et les courses de données. Peut-être que je ne comprends pas très bien la quantité de méthodologie concurrente saine dans le compilateur et la quantité de celle-ci dans le système d'exploitation.

35
thb

Une meilleure histoire de concurrence est l'un des principaux objectifs du projet Rust, donc des améliorations doivent être attendues, à condition que nous fassions confiance au projet pour atteindre ses objectifs. Avertissement complet: J'ai une opinion élevée de = Rust et j'y suis investi. Comme demandé, je vais essayer d'éviter les jugements de valeur et décrire les différences plutôt que ( IMHO) améliorations .

Rouille sûre et dangereuse

"Rust" est composé de deux langages: un qui essaie très fort de vous isoler des dangers de la programmation des systèmes, et un plus puissant sans de telles aspirations.

Dangereux Rust est un langage méchant et brutal qui ressemble beaucoup à C++. Il vous permet de faire des choses arbitrairement dangereuses, de parler au matériel, de (mal) gérer la mémoire manuellement, de vous tirer dessus dans le foot, etc. Cela ressemble beaucoup à C et C++ dans la mesure où l'exactitude du programme est entre vos mains et entre les mains de tous les autres programmeurs impliqués. Vous optez pour ce langage avec le mot clé unsafe, et comme en C et C++, une seule erreur dans un seul emplacement peut faire planter l'ensemble du projet.

Safe Rust est le "défaut", la grande majorité du code Rust est sûr, et si vous ne mentionnez jamais le mot-clé unsafe dans votre code, vous ne quittez jamais la langue sûre. Le reste de la publication se préoccupera principalement de cette langue, car le code unsafe peut casser toutes les garanties que la sécurité Rust = travaille si dur pour vous donner. D'un autre côté, le code unsafe est pas mal et n'est pas traité comme tel par la communauté (il est cependant fortement déconseillé lorsqu'il n'est pas nécessaire) .

C'est dangereux, oui, mais aussi important, car cela permet de construire les abstractions que le code sécurisé utilise. Un bon code non sécurisé utilise le système de type pour empêcher les autres de l'utiliser à mauvais escient, et donc la présence de code non sûr dans un programme Rust ne doit pas perturber le code sécurisé. Toutes les différences suivantes existent parce que les systèmes de type Rust possède des outils que C++ ne possède pas, et parce que le code dangereux qui implémente les abstractions de concurrence utilise ces outils efficacement.

Pas de différence: mémoire partagée/mutable

Bien que Rust met davantage l'accent sur le passage des messages et contrôle très strictement la mémoire partagée, il n'exclut pas la simultanéité de la mémoire partagée et prend explicitement en charge 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 liée persistante/immuable dans la bibliothèque standard. Il y a std::collections::LinkedList Mais c'est comme std::list En C++ et déconseillé pour les mêmes raisons que std::list (Mauvaise utilisation du cache).

Cependant, en référence au titre de cette section ("mémoire partagée/mutable"), Rust a une différence avec C++: il encourage fortement que la mémoire soit "partagée XOR mutable ", c'est-à-dire que la mémoire n'est jamais partagée et mutable en même temps. Mutez la mémoire comme vous le souhaitez" dans l'intimité de votre propre thread ", pour ainsi dire. Comparez cela avec C++ où la mémoire mutable partagée est la valeur par défaut option et largement utilisé.

Bien que le paradigme mutable xor-mutable soit très important pour les différences ci-dessous, il s'agit également d'un paradigme de programmation assez différent qui prend un certain temps pour s'y habituer, et qui impose des restrictions importantes. Parfois, il faut se retirer de ce paradigme, par exemple, avec les 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 elle exclut les lectures et écritures simultanées (alors qu'un thread écrit, aucun autre thread ne peut lire ou écrire).

Non-différence: les races de données sont un comportement indéfini (UB)

Si vous déclenchez une course aux données dans le code Rust, c'est terminé, comme en C++. Tous les paris sont désactivés et le compilateur peut faire ce qu'il veut.

Cependant, il est une garantie solide que sûr Rust n'a pas de race de données (ou tout autre UB d'ailleurs). 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 les bibliothèques tierces mais à l'exclusion de la bibliothèque standard) qui déclenche UB, alors c'est considéré comme un bogue et sera corrigé (cela s'est déjà produit plusieurs fois). Ceci est bien sûr en contraste frappant avec C++, où il est trivial d'écrire des programmes avec UB.

Différence: discipline de verrouillage stricte

Contrairement à C++, un verrou dans Rust (std::sync::Mutex, std::sync::RwLock, Etc.) possède les données qu'il protège. Au lieu de prendre un verrou puis de manipuler de la mémoire partagée associée au verrou uniquement dans la documentation, les données partagées sont inaccessibles tant que vous ne tenez pas le verrou. Un garde RAII garde le verrou et donne simultanément accès aux données verrouillées (cela pourrait être implémenté par C++, mais pas par les verrous std::). Le système à vie garantit que vous ne pouvez pas continuer à accéder aux données après avoir relâché le verrou (laissez tomber le garde 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, la mémoire partagée potentiellement non synchronisée nécessite unsafe.

Différence: prévention du partage accidentel

Bien que vous puissiez partager la mémoire, 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 à vie garantit que vous ne conservez aucune référence aux données après l'avoir envoyée à un autre thread. Pour partager des données derrière un verrou, vous construisez explicitement le verrou et le donnez à un autre thread. Pour partager de la mémoire non synchronisée avec unsafe, vous devez bien utiliser unsafe.

Cela rejoint le point suivant:

Différence: suivi de la sécurité des threads

Le système de type de Rust suit une certaine notion de sécurité du fil. Plus précisément, le trait Sync dénote les types qui peuvent être partagés par plusieurs threads sans risque de courses de données, tandis que Send marque ceux qui peuvent être déplacés d'un thread à un autre. Ceci est imposé par le compilateur tout au long du programme, et donc les concepteurs de bibliothèques osent faire des optimisations qui seraient stupidement dangereuses sans ces vérifications statiques. Par exemple, le std::shared_ptr De C++ qui utilise toujours des opérations atomiques pour manipuler son compte de référence, pour éviter UB si un shared_ptr Est utilisé par plusieurs threads. Rust a Rc et Arc, qui diffèrent seulement en ce que Rc utilise des opérations de recomptage non atomiques et n'est pas threadsafe (ie doesn n'implémentez pas Sync ou Send) tandis que Arc ressemble beaucoup à shared_ptr (et implémente les deux traits).

Notez que si un type n'utilise pas unsafe pour implémenter manuellement la synchronisation, la présence ou l'absence des traits est correctement déduite.

Différence: règles très strictes

Si le compilateur ne peut pas être absolument sûr qu'un certain code est exempt de courses de données et d'autres UB, il ne compilera pas, point . Les règles susmentionnées 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 verrou difficile, mais cela pourrait également être quelque chose d'aussi banal que "J'écris à des emplacements aléatoires dans un tableau partagé mais les indices 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 reformuler le code de sorte que le compilateur puisse voir son exactitude (souvent faisable, parfois assez difficile, parfois impossible), ou vous passez dans unsafe code. Pourtant, c'est une surcharge mentale supplémentaire, et Rust ne vous donne aucune garantie quant à l'exactitude du code unsafe.

Différence: moins d'outils

En raison des différences susmentionnées, dans Rust il est beaucoup plus rare que l'on écrive du code qui peut avoir une course de données (ou une utilisation après free, ou un double free, ou ...). c'est Nice, cela a le malheureux effet secondaire que l'écosystème pour localiser de telles erreurs est encore plus sous-développé que ce à quoi on pourrait s'attendre étant donné la jeunesse et la petite taille de la communauté.

Alors que des outils tels que valgrind et le désinfectant de threads de LLVM pourraient en principe être appliqués au code Rust, si cela fonctionne réellement varie d'un outil à l'autre (et même ceux qui fonctionnent peuvent être difficiles à configurer, en particulier car vous ne trouverez peut-être pas de ressources à jour sur la façon de le faire.) Cela n'aide pas vraiment que Rust manque actuellement de véritables spécifications et en particulier un modèle de mémoire formel.

En bref, écrire unsafe Rust est correctement plus difficile que d'écrire correctement le code C++, malgré les deux les langues étant à 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 code unsafe, alors qu'un programme C++ est, bien, entièrement C++.

56
user7043