Wie unterscheidet sich Rust von den Nebenläufigkeitsfunktionen von C ++?

35

Fragen

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

Wie verbessert oder weicht idiomatic Rust von den Nebenläufigkeitsfunktionen von idiomatic C ++ ab?

Ist die Verbesserung (oder Divergenz) hauptsächlich 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ät von C ++ 14 beizubringen, und etwas fühlt sich nicht ganz richtig an. Etwas fühlt sich ab. Was fühlt sich an? Schwer zu sagen.

Es fühlt sich fast so an, als würde der Compiler nicht wirklich versuchen, mir beim Schreiben korrekter Programme zu helfen, 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 bei der Parallelität noch unter einem subtilen, fehlerhaften Konzept leide. Vielleicht habe ich Bartosz Milewskis Spannung zwischen Stateful Programming und Datenrennen noch nicht geschürt. Vielleicht verstehe ich nicht so recht, wie viel von der Sound-Concurrent-Methodik im Compiler steckt und wie viel davon im Betriebssystem.

thb
quelle

Antworten:

56

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 unsafein Ihrem Code erwähnen , verlassen Sie niemals die sichere Sprache. Der Rest des Beitrags wird sich hauptsächlich mit dieser Sprache befassen, da unsafeCode alle Garantien brechen kann, die Safe Rust Ihnen so schwer macht. Auf der anderen Seite ist unsafeCode 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::LinkedListaber es ist wie std::listin 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 ( AtomicUsizeist 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::RwLockusw.) 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 unsafeIhnen 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 SyncMerkmal Typen, die von mehreren Threads ohne Risiko von Datenrassen gemeinsam genutzt werden können, während Senddiejenigen 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_ptrdie immer atomare Operationen verwenden, um ihren Referenzzähler zu manipulieren, um UB zu vermeiden, wenn a shared_ptrzufällig von mehreren Threads verwendet wird. Rust hat Rcund Arc, die sich nur dadurch unterscheiden, dass sie Rc nicht-atomare Refcount-Operationen verwenden und nicht threadsicher sind (dh nicht implementieren Syncoder Send), während Arces sehr ähnlich istshared_ptr (und implementiert beide Merkmale).

Beachten Sie, dass , wenn ein Typ nicht nicht verwenden unsafemanuell 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 unsafeCode. Trotzdem ist es ein zusätzlicher mentaler Aufwand und Rust gibt Ihnen keine Garantie für die Richtigkeit des unsafeCodes.

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 unsafekorrekte 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 unsafeCodes enthält, wohingegen ein C ++ - Programm vollständig C ++ ist.


quelle
6
Wo ist auf meinem Bildschirm der +25 Upvote-Schalter? Ich kann es nicht finden! Diese informative Antwort wird sehr geschätzt. Ich habe keine offensichtlichen Fragen zu den behandelten Punkten. Also zu anderen Punkten: Wenn ich die Dokumentation von Rust verstehe, hat Rust [a] integrierte Testeinrichtungen und [b] ein Build-System namens Cargo. Sind diese Ihrer Meinung nach einigermaßen produktionsreif? Ist es in Bezug auf Cargo gut gelaunt, wenn ich Shell, Python- und Perl-Skripte, LaTeX-Kompilierung usw. zum Build-Prozess hinzufüge?
8.
2
@thb Das Testmaterial ist sehr nackt (z. B. keine Verspottung), aber funktional. Cargo funktioniert recht gut, obwohl der Fokus auf Rust und Reproduzierbarkeit bedeutet, dass es möglicherweise nicht die beste Option ist, um alle Schritte vom Quellcode bis zu den endgültigen Artefakten abzudecken. Sie können Build-Skripte schreiben , dies ist jedoch möglicherweise nicht für alle von Ihnen erwähnten Dinge geeignet. (Leute verwenden jedoch regelmäßig Build-Skripte, um C-Bibliotheken zu kompilieren oder vorhandene Versionen von C-Bibliotheken zu finden, damit Cargo nicht aufhört zu arbeiten, wenn Sie mehr als reines Rust verwenden.)
2
Übrigens, was es wert ist, Ihre Antwort sieht ziemlich schlüssig aus. Da ich C ++ mag, weil C ++ annehmbare Möglichkeiten für fast alles hat, was ich tun musste, und weil C ++ stabil und weit verbreitet ist, war ich bisher ziemlich zufrieden damit, C ++ für jeden möglichen nicht leichten Zweck zu verwenden (ich habe nie Interesse an Java entwickelt) , beispielsweise). Aber jetzt haben wir Parallelität, und C ++ 14 scheint mir damit zu kämpfen. Ich habe seit einem Jahrzehnt nicht mehr freiwillig eine neue Programmiersprache ausprobiert, aber (es sei denn, Haskell scheint eine bessere Option zu sein) ich denke, dass ich Rust ausprobieren muss.
8.
Note that if a type doesn't use unsafe to manually implement synchronization, the presence or absence of the traits are inferred correctly.eigentlich geht es ja noch mit unsafeelementen. Nur unformatierte Zeiger sind weder Syncnoch, Sharewas bedeutet, dass eine Struktur, die sie enthält, standardmäßig auch keine enthält.
Hauleth
@ ŁukaszNiemier Es kann passieren, dass es gut läuft, aber es gibt eine Milliarde Möglichkeiten, wie ein unsicherer Typ auftauchen kann Sendoder Syncauch wenn es eigentlich nicht sein sollte.
-2

Rust ist auch sehr ähnlich wie Erlang and Go. Die Kommunikation erfolgt über Kanäle mit Puffern und bedingtem Warten. Genau wie Go lockert es die Einschränkungen von Erlang, indem es Ihnen ermöglicht, gemeinsam genutzten Speicher zu nutzen, Atomic Reference Counting und Sperren zu unterstützen und Kanäle von Thread zu Thread weiterzuleiten.

Rust geht jedoch noch einen Schritt weiter. Während Go darauf vertraut, dass Sie das Richtige tun, beauftragt Rust einen Mentor, der bei Ihnen sitzt und sich beschwert, wenn Sie versuchen, das Falsche zu tun. Rusts Mentor ist der Compiler. Es führt eine ausgefeilte Analyse durch, um den Besitz von Werten zu bestimmen, die an Threads übergeben werden, und um Kompilierungsfehler anzuzeigen, wenn potenzielle Probleme vorliegen.

Es folgt ein Zitat aus RUST-Dokumenten.

Die Besitzregeln spielen beim Senden von Nachrichten eine wichtige Rolle, da sie uns helfen, sicheren, gleichzeitigen Code zu schreiben. Das Vermeiden von Fehlern bei der gleichzeitigen Programmierung ist der Vorteil, den wir dadurch erzielen, dass wir uns die Mühe machen, in allen unseren Rust-Programmen über die Eigentumsverhältnisse nachzudenken. - Message-Passing mit Besitz von Werten.

Wenn Erlang drakonisch ist und Go ein freier Staat ist, dann ist Rust ein Kindermädchenstaat.

Weitere Informationen finden Sie unter Parallelitätsideologien von Programmiersprachen: Java, C #, C, C +, Go und Rust

srinath_perera
quelle
2
Willkommen bei Stack Exchange! Bitte beachten Sie, dass Sie dies immer explizit angeben müssen, wenn Sie auf Ihren eigenen Blog verlinken. Siehe die Hilfe .
Glorfindel