it-swarm.com.de

Wie unterscheidet sich Rust von den Parallelitätsfunktionen von C ++?

Fragen

Ich versuche zu verstehen, ob Rust die Parallelitätsfunktionen von C++ grundlegend und ausreichend verbessert, um zu entscheiden, ob ich die Zeit zum Erlernen von Rust verwenden soll.

Wie verbessert sich idiomatic Rust) gegenüber den Parallelitätsfunktionen von idiomatic C++ oder weicht jedenfalls davon ab?

Ist die Verbesserung (oder Divergenz) meist syntaktisch oder handelt es sich im Wesentlichen um eine Verbesserung (Divergenz) des Paradigmas? Oder ist es etwas anderes? Oder ist es überhaupt keine Verbesserung (Divergenz)?


Begründung

Ich habe kürzlich versucht, mir die Parallelitätsfunktionen von C++ 14 beizubringen, und etwas fühlt sich nicht ganz richtig an. Etwas fühlt sich nicht an. Was fühlt sich schlecht an? Schwer zu sagen.

Es fühlt sich fast so an, als würde der Compiler nicht wirklich versuchen, mir zu helfen, korrekte Programme zu schreiben, wenn es um Parallelität geht. Es fühlt sich fast so an, als würde ich eher einen Assembler als einen Compiler verwenden.

Zugegeben, es ist durchaus wahrscheinlich, dass ich in Bezug auf die Parallelität noch unter einem subtilen, fehlerhaften Konzept leide. Vielleicht verstehe ich Bartosz Milewskis Spannung zwischen staatlicher Programmierung und Datenrennen noch nicht. Vielleicht verstehe ich nicht ganz, wie viel Sound-Concurrent-Methodik im Compiler und wie viel davon im Betriebssystem enthalten ist.

35
thb

Eine bessere Parallelitätsgeschichte ist eines der Hauptziele des Rust -Projekts. Daher sollten Verbesserungen erwartet werden, vorausgesetzt, wir vertrauen darauf, dass das Projekt seine Ziele erreicht. Vollständiger Haftungsausschluss: Ich habe eine hohe Meinung von Rust und bin darin investiert. Wie gewünscht werde ich versuchen, Werturteile zu vermeiden und Unterschiede anstatt (IMHO) Verbesserungen zu beschreiben.

Sicherer und unsicherer Rost

"Rust" besteht aus zwei Sprachen: Eine, die sich sehr bemüht, Sie von den Gefahren der Systemprogrammierung zu isolieren, und eine leistungsstärkere ohne solche Bestrebungen.

Unsicher Rust ist eine böse, brutale Sprache, die sich sehr nach C++ anfühlt. Es ermöglicht Ihnen, willkürlich gefährliche Dinge zu tun, mit der Hardware zu sprechen, den Speicher manuell (falsch) zu verwalten, sich in den Fuß zu schießen usw. Es ist C und C++ sehr ähnlich, da die Korrektheit des Programms letztendlich in Ihren Händen liegt und die Hände aller anderen daran beteiligten Programmierer. Sie aktivieren diese Sprache mit dem Schlüsselwort unsafe. Wie in C und C++ kann ein einzelner Fehler an einem einzelnen Speicherort zum Absturz des gesamten Projekts führen.

Sicher Rust ist die "Standardeinstellung", die überwiegende Mehrheit des Rust Codes ist sicher, und wenn Sie das Schlüsselwort unsafe in Ihrem Code nie erwähnen, verlassen Sie es nie die sichere Sprache. Der Rest des Beitrags befasst sich hauptsächlich mit dieser Sprache, da der Code unsafe alle Garantien aufheben kann, die sicheres Rust so schwer zu bieten hat. Auf der anderen Seite ist unsafe Code nicht böse und wird von der Community nicht als solcher behandelt (es wird jedoch dringend davon abgeraten, wenn dies nicht erforderlich ist).

Es ist gefährlich, ja, aber auch wichtig, weil es das Erstellen der Abstraktionen ermöglicht, die sicherer Code verwendet. Guter unsicherer Code verwendet das Typsystem, um zu verhindern, dass andere ihn missbrauchen. Daher muss das Vorhandensein von unsicherem Code in einem Rust -Programm den sicheren Code nicht stören. Alle folgenden Unterschiede bestehen, weil die Typsysteme von Rust über Tools verfügen, über die C++ nicht verfügt, und weil der unsichere Code, der die Parallelitätsabstraktionen implementiert, diese Tools effektiv verwendet.

Kein Unterschied: Shared/Mutable Memory

Obwohl Rust mehr Wert auf die Weitergabe von Nachrichten legt und den gemeinsam genutzten Speicher sehr streng kontrolliert, schließt es die Parallelität des gemeinsam genutzten Speichers nicht aus und unterstützt explizit die allgemeinen Abstraktionen (Sperren, atomare Operationen, Bedingungsvariablen, gleichzeitige Sammlungen).

Darüber hinaus mag Rust wie C++ und im Gegensatz zu funktionalen Sprachen traditionelle imperative Datenstrukturen sehr. In der Standardbibliothek gibt es keine dauerhafte/unveränderliche verknüpfte Liste. Es gibt std::collections::LinkedList, Aber es ist wie std::list In C++ und wird aus den gleichen Gründen wie std::list (Schlechte Verwendung des Caches) nicht empfohlen.

In Bezug auf den Titel dieses Abschnitts ("gemeinsam genutzter/veränderbarer Speicher") weist Rust jedoch einen Unterschied zu C++ auf: Es wird dringend empfohlen, dass der Speicher "gemeinsam genutzter XOR veränderlich" ist. Das heißt, dieser Speicher wird niemals gleichzeitig geteilt und veränderbar. Mutieren Sie das Gedächtnis sozusagen "in der Privatsphäre Ihres eigenen Threads". Vergleichen Sie dies mit C++, wo gemeinsam genutzter veränderbarer Speicher die Standardoption ist und weit verbreitet ist.

Während das Shared-Xor-Mutable-Paradigma für die folgenden Unterschiede sehr wichtig ist, ist es auch ein ganz anderes Programmierparadigma, an das man sich erst nach einiger Zeit gewöhnt und das erhebliche Einschränkungen mit sich bringt. Gelegentlich muss man dieses Paradigma ablehnen, z. B. bei Atomtypen (AtomicUsize ist die Essenz des gemeinsamen veränderlichen Gedächtnisses). Beachten Sie, dass Sperren auch der Shared-Xor-Mutable-Regel entsprechen, da sie das Lesen und Schreiben von gleichzeitig ausschließen (während ein Thread schreibt, können keine anderen Threads lesen oder schreiben).

Kein Unterschied: Datenrennen sind undefiniertes Verhalten (UB)

Wenn Sie ein Datenrennen in Rust Code auslösen, ist das Spiel genau wie in C++ beendet. Alle Wetten sind geschlossen und der Compiler kann tun, was er will.

Es ist jedoch eine harte Garantie, dass sicherer Rust Code keine Datenrassen hat (oder irgendein UB für diese Angelegenheit). Dies erstreckt sich sowohl auf die Kernsprache als auch auf die Standardbibliothek. Wenn Sie ein Rust -Programm schreiben können, das nicht unsafe verwendet (einschließlich in Bibliotheken von Drittanbietern, jedoch ohne die Standardbibliothek), das UB auslöst, wird dies als Fehler angesehen und behoben (das ist schon mehrmals passiert). Dies steht natürlich in krassem Gegensatz zu C++, wo es trivial ist, Programme mit UB zu schreiben.

Unterschied: Strenge Sperrdisziplin

Im Gegensatz zu C++ ist eine Sperre in Rust (std::sync::Mutex, std::sync::RwLock Usw.) besitzt die Daten, die geschützt werden. Anstatt eine Sperre aufzuheben und dann einen gemeinsam genutzten Speicher zu bearbeiten, der der Sperre nur in der Dokumentation zugeordnet ist, kann auf die gemeinsam genutzten Daten nicht zugegriffen werden, solange Sie die Sperre nicht halten. Ein RAII-Wächter behält die Sperre bei und gewährt gleichzeitig Zugriff auf die gesperrten Daten (dies könnte von C++ implementiert werden, jedoch nicht von den Sperren std::). Das lebenslange System stellt sicher, dass Sie nach dem Aufheben der Sperre nicht mehr auf die Daten zugreifen können (lassen Sie den RAII-Schutz fallen).

Sie können natürlich eine Sperre haben, die keine nützlichen Daten enthält (Mutex<()>), und nur etwas Speicher freigeben, ohne ihn explizit mit dieser Sperre zu verknüpfen. Für einen möglicherweise nicht synchronisierten gemeinsam genutzten Speicher ist jedoch unsafe erforderlich.

Unterschied: Verhinderung des versehentlichen Teilens

Obwohl Sie gemeinsam genutzten Speicher haben können, teilen Sie diesen nur, wenn Sie explizit danach fragen. Wenn Sie beispielsweise die Nachrichtenübermittlung verwenden (z. B. die Kanäle von std::sync), Stellt das Lebensdauer-System sicher, dass Sie keine Verweise auf die Daten behalten, nachdem Sie sie an einen anderen Thread gesendet haben. Um Daten hinter einer Sperre freizugeben, erstellen Sie die Sperre explizit und geben sie an einen anderen Thread weiter. Um nicht synchronisierten Speicher mit unsafe zu teilen, müssen Sie unsafe verwenden.

Dies knüpft an den nächsten Punkt an:

Unterschied: Thread-Sicherheits-Tracking

Das Typsystem von Rust verfolgt einige Vorstellungen von Gewindesicherheit. Insbesondere kennzeichnet das Merkmal Sync Typen, die von mehreren Threads gemeinsam genutzt werden können, ohne dass das Risiko von Datenrennen besteht, während Send diejenigen kennzeichnet, die von einem Thread in einen anderen verschoben werden können. Dies wird vom Compiler während des gesamten Programms erzwungen, und daher wagen Bibliotheksdesigner Optimierungen, die ohne diese statischen Überprüfungen dumm gefährlich wären. Zum Beispiel C++ 's std::shared_ptr, Das immer atomare Operationen verwendet, um seinen Referenzzähler zu manipulieren, um UB zu vermeiden, wenn ein shared_ptr Von mehreren Threads verwendet wird. Rust hat Rc und Arc, die sich nur dadurch unterscheiden, dass Rc nichtatomare Refcount-Operationen verwendet und nicht threadsicher ist (dh nicht implementiert) Sync oder Send), während Arcshared_ptr Sehr ähnlich ist (und beide Merkmale implementiert).

Beachten Sie, dass, wenn ein Typ nichtunsafe verwendet, um die Synchronisation manuell zu implementieren, das Vorhandensein oder Fehlen der Merkmale korrekt abgeleitet wird.

Unterschied: Sehr strenge Regeln

Wenn der Compiler nicht absolut sicher sein kann, dass ein Teil des Codes frei von Datenrennen und anderen UB ist, wird nicht kompiliert, Punkt. Die oben genannten Regeln und andere Tools können Sie ziemlich weit bringen, aber früher oder später möchten Sie etwas tun, das korrekt ist, aber aus subtilen Gründen, die dem Compiler nicht auffallen. Es könnte eine knifflige, sperrenfreie Datenstruktur sein, aber es könnte auch etwas so Alltägliches sein wie "Ich schreibe an zufällige Orte in einem gemeinsam genutzten Array, aber die Indizes werden so berechnet, dass jeder Ort nur von einem Thread beschrieben wird".

An diesem Punkt können Sie entweder in die Kugel beißen und ein wenig unnötige Synchronisation hinzufügen oder den Code so umformulieren, dass der Compiler seine Richtigkeit erkennt (oft machbar, manchmal ziemlich schwer, manchmal unmöglich), oder Sie fallen in unsafe Code. Trotzdem ist es ein zusätzlicher mentaler Aufwand, und Rust gibt Ihnen keine Garantie für die Richtigkeit des unsafe - Codes.

Unterschied: Weniger Werkzeuge

Aufgrund der oben genannten Unterschiede ist es in Rust viel seltener, dass man Code schreibt, der ein Datenrennen haben kann (oder eine Verwendung nach frei oder doppelt frei oder ...). Dies ist zwar schön, hat aber den unglücklichen Nebeneffekt, dass das Ökosystem zum Aufspüren solcher Fehler noch unterentwickelter ist, als man es angesichts der Jugend und der geringen Größe der Gemeinde erwarten würde.

Während Tools wie valgrind und LLVMs Thread-Desinfektionsprogramm im Prinzip auf Rust Code angewendet werden können, variiert die Frage, ob dies tatsächlich funktioniert, von Tool zu Tool (und selbst diejenigen, die funktionieren, sind möglicherweise schwer einzurichten, insbesondere seit Sie dies tun keine aktuellen Ressourcen dazu finden). Es hilft nicht wirklich, dass Rust derzeit keine echte Spezifikation und insbesondere kein formales Speichermodell besitzt.

Kurz gesagt, das korrekte Schreiben von unsafe Rust Code ist schwieriger als das korrekte Schreiben von C++ - Code, obwohl beide Sprachen in Bezug auf Fähigkeiten und Risiken in etwa vergleichbar sind. Dies muss natürlich gegen die Tatsache abgewogen werden, dass ein typisches Rust -Programm nur einen relativ kleinen Bruchteil des unsafe - Codes enthält, während ein C++ - Programm vollständig C++ ist.

56
user7043