Eine bessere Parallelität ist eines der Hauptziele des Rust-Projekts. Daher sollten Verbesserungen erwartet werden, vorausgesetzt, wir vertrauen darauf, dass das Projekt seine Ziele erreicht. Voller Haftungsausschluss: Ich habe eine hohe Meinung von Rust und bin darin investiert. Wie gewünscht werde ich versuchen, Werturteile zu vermeiden und Unterschiede zu beschreiben, anstatt (IMHO) Verbesserungen vorzunehmen .
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 leistungsfähigere, die keine derartigen Bestrebungen hat.
Unsicherer Rost ist eine böse, brutale Sprache, die sich sehr nach C ++ anfühlt. Es erlaubt Ihnen, willkürlich gefährliche Dinge zu tun, mit der Hardware zu sprechen, den Speicher manuell (falsch) zu verwalten, sich selbst in den Fuß zu schießen usw. Es ist C und C ++ sehr ähnlich, da die Richtigkeit des Programms letztendlich in Ihren Händen liegt und die Hände aller anderen daran beteiligten Programmierer. Sie entscheiden sich mit dem Schlüsselwort für diese Sprache unsafe
, und wie in C und C ++ kann ein einzelner Fehler an einer einzelnen Stelle das gesamte Projekt zum Absturz bringen.
Safe Rust ist der "Standard", die überwiegende Mehrheit des Rust-Codes ist sicher, und wenn Sie das Schlüsselwort nie unsafe
in Ihrem Code erwähnen , verlassen Sie niemals die sichere Sprache. Der Rest des Beitrags wird sich hauptsächlich mit dieser Sprache befassen, da unsafe
Code alle Garantien brechen kann, die Safe Rust Ihnen so schwer macht. 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 ermöglicht, die von sicherem Code verwendeten Abstraktionen zu erstellen. 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, da Rusts Typsysteme Tools enthalten, über die C ++ nicht verfügt, und weil der unsichere Code, der die Nebenläufigkeitsabstraktionen implementiert, diese Tools effektiv verwendet.
Non-difference: Shared / Mutable Memory
Obwohl Rust mehr Wert auf die Weitergabe von Nachrichten legt und Shared Memory streng kontrolliert, schließt es Shared Memory-Parallelität 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. Es gibt keine dauerhafte / unveränderliche verknüpfte Liste in der Standardbibliothek. Es gibt std::collections::LinkedList
aber es ist wie std::list
in C ++ und aus den gleichen Gründen wie std::list
(schlechte Nutzung des Cache) entmutigt .
In Bezug auf den Titel dieses Abschnitts ("Shared / Mutable Memory") weist Rust jedoch einen Unterschied zu C ++ auf: Es wird ausdrücklich empfohlen, dass der Speicher "Shared XOR Mutable" ist, dh, dass der Speicher niemals gemeinsam genutzt und gleichzeitig veränderbar ist Zeit. Mutieren Sie das Gedächtnis sozusagen "in der Privatsphäre Ihres eigenen Threads". Vergleichen Sie dies mit C ++, wo Shared Mutable Memory 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öhnen muss und das erhebliche Einschränkungen auferlegt. Gelegentlich muss man sich von diesem Paradigma abkoppeln, z. B. bei atomaren Typen ( AtomicUsize
ist die Essenz eines gemeinsamen veränderlichen Gedächtnisses). Beachten Sie, dass Sperren auch die Shared-Xor-Mutable-Regel einhalten, da sie gleichzeitige Lese- und Schreibvorgänge ausschließen (während ein Thread schreibt, können keine anderen Threads lesen oder schreiben).
Non-difference: Datenrennen sind undefiniertes Verhalten (UB)
Wenn Sie in Rust Code ein Datenrennen auslösen, ist das Spiel wie in C ++ beendet. Alle Wetten sind deaktiviert und der Compiler kann tun, was ihm gefällt.
Es ist jedoch eine harte Garantie, dass der sichere Rust-Code keine Datenrennen (oder UBs) hat. Dies erstreckt sich sowohl auf die Kernsprache als auch auf die Standardbibliothek. Wenn Sie ein Rust-Programm schreiben können, das nicht verwendet wird unsafe
(einschließlich in Bibliotheken von Drittanbietern, aber ausschließlich der Standardbibliothek), das UB auslöst, wird dies als Fehler angesehen und behoben (dies ist bereits mehrmals geschehen). 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 ++, ein Schloss in Rust ( std::sync::Mutex
, std::sync::RwLock
usw.) besitzt , die Daten , die sie beschützt. Anstatt eine Sperre aufzuheben und dann einen Teil des gemeinsam genutzten Speichers zu manipulieren, der nur in der Dokumentation mit der Sperre verknüpft ist, kann nicht auf die gemeinsam genutzten Daten 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, aber nicht von den std::
Sperren). Das Lifetime-System stellt sicher, dass Sie nach dem Aufheben der Sperre nicht mehr auf die Daten zugreifen können (RAII-Schutz fallen lassen).
Sie können natürlich eine Sperre haben, die keine nützlichen Daten enthält ( Mutex<()>
), und nur einen Teil des Speichers gemeinsam nutzen, ohne ihn explizit mit dieser Sperre zu verknüpfen. Möglicherweise nicht synchronisierter gemeinsamer Speicher erfordert jedoch unsafe
.
Unterschied: Verhinderung von versehentlichem Teilen
Sie können zwar gemeinsam genutzten Speicher haben, diesen jedoch nur dann freigeben, wenn Sie ausdrücklich danach fragen. Wenn Sie beispielsweise die Nachrichtenübermittlung verwenden (z. B. die Kanäle von std::sync
), stellt das Lifetime-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 sie einem anderen Thread. Um nicht synchronisierten Speicher mit unsafe
Ihnen zu teilen , müssen Sie verwenden unsafe
.
Dies knüpft an den nächsten Punkt an:
Unterschied: Fadensicherheitsverfolgung
Das Typensystem von Rust verfolgt eine Vorstellung von der Fadensicherheit. Insbesondere bezeichnet das Sync
Merkmal Typen, die von mehreren Threads ohne Risiko von Datenrassen gemeinsam genutzt werden können, während Send
diejenigen markiert sind, die von einem Thread zum anderen verschoben werden können. Dies wird vom Compiler im gesamten Programm erzwungen, und daher wagen Bibliotheksentwickler Optimierungen, die ohne diese statischen Überprüfungen dumm und gefährlich wären. Zum Beispiel C ++ 's, std::shared_ptr
die immer atomare Operationen verwenden, um ihren Referenzzähler zu manipulieren, um UB zu vermeiden, wenn a shared_ptr
zufällig von mehreren Threads verwendet wird. Rust hat Rc
und Arc
, die sich nur dadurch unterscheiden, dass sie Rc
nicht-atomare Refcount-Operationen verwenden und nicht threadsicher sind (dh nicht implementieren Sync
oder Send
), während Arc
es sehr ähnlich istshared_ptr
(und implementiert beide Merkmale).
Beachten Sie, dass , wenn ein Typ nicht nicht verwenden unsafe
manuell Synchronisation zu implementieren, das Vorhandensein oder Fehlen der Merkmale korrekt abgeleitet werden.
Unterschied: Sehr strenge Regeln
Wenn der Compiler nicht absolut sicher sein kann, dass ein Teil des Codes frei von Datenrassen und anderen UB ist, wird er nicht kompiliert, Punkt . Die oben genannten Regeln und andere Tools können Sie weit bringen, aber früher oder später werden Sie etwas tun wollen, das korrekt ist, aber aus subtilen Gründen, die dem Compiler nicht auffallen. Es könnte sich um eine knifflige gesperrte Datenstruktur handeln, aber auch um etwas so Alltägliches wie "Ich schreibe an zufällige Stellen in einem gemeinsam genutzten Array, aber die Indizes werden so berechnet, dass jede Stelle von nur 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 Korrektheit erkennen kann (oft machbar, manchmal ziemlich schwer, manchmal unmöglich), oder Sie springen in den 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 möglicherweise ein Datenrennen hat (oder eine Verwendung nach "free" oder "double free" oder ...). Das 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 Gemeinschaft erwarten würde.
Während Tools wie valgrind und LLVMs Thread Sanitizer im Prinzip auf Rust-Code angewendet werden könnten, ist es von Tool zu Tool unterschiedlich, ob dies tatsächlich funktioniert (und selbst die, die funktionieren, sind möglicherweise schwierig einzurichten, zumal Sie möglicherweise keine Lösung finden -Date Ressourcen, wie es geht). Es hilft nicht wirklich, dass Rust derzeit eine echte Spezifikation und insbesondere ein formales Gedächtnismodell fehlt.
Kurz gesagt, das unsafe
korrekte Schreiben von 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, wohingegen ein C ++ - Programm vollständig C ++ ist.