Java und C # bieten Speichersicherheit, indem sie Array-Grenzen und Zeiger-Dereferenzen überprüfen.
Welche Mechanismen könnten in eine Programmiersprache implementiert werden, um die Möglichkeit von Rennbedingungen und Deadlocks zu verhindern?
Java und C # bieten Speichersicherheit, indem sie Array-Grenzen und Zeiger-Dereferenzen überprüfen.
Welche Mechanismen könnten in eine Programmiersprache implementiert werden, um die Möglichkeit von Rennbedingungen und Deadlocks zu verhindern?
Antworten:
Rennen treten auf, wenn Sie gleichzeitig ein Aliasing für ein Objekt haben und mindestens einer der Aliase mutiert.
Um Rennen zu verhindern, müssen Sie eine oder mehrere dieser Bedingungen unwahr machen.
Verschiedene Ansätze befassen sich mit verschiedenen Aspekten. Die funktionale Programmierung betont die Unveränderlichkeit, wodurch die Veränderlichkeit beseitigt wird. Locking / Atomics entfernen die Gleichzeitigkeit. Affine Typen entfernen das Aliasing (Rust entfernt veränderbares Aliasing). Schauspieler-Modelle entfernen normalerweise Aliasing.
Sie können die Objekte einschränken, die als Alias verwendet werden können, um sicherzustellen, dass die oben genannten Bedingungen vermieden werden. Hier kommen Kanäle und / oder Nachrichtenübermittlungsstile ins Spiel. Sie können keinen Alias für einen beliebigen Speicher verwenden, sondern nur das Ende eines Kanals oder einer Warteschlange, die so angeordnet ist, dass sie rennfrei sind. In der Regel durch Vermeidung von Gleichzeitigkeit, dh Sperren oder Atomics.
Der Nachteil dieser verschiedenen Mechanismen ist, dass sie die Programme einschränken, die Sie schreiben können. Je stumpfer die Einschränkung, desto weniger Programme. Also kein Aliasing oder keine Veränderbarkeitsarbeit, und sie sind leicht zu begründen, aber sehr einschränkend.
Deshalb sorgt Rust für so viel Aufsehen. Es ist eine Ingenieursprache (im Gegensatz zur akademischen), die Aliasing und Veränderlichkeit unterstützt, aber vom Compiler überprüfen lässt, ob sie nicht gleichzeitig auftreten. Obwohl dies nicht das Ideal ist, kann eine größere Klasse von Programmen sicher geschrieben werden als viele seiner Vorgänger.
Java und C # bieten Speichersicherheit, indem sie Array-Grenzen und Zeiger-Dereferenzen überprüfen.
Es ist wichtig, zuerst darüber nachzudenken, wie C # und Java dies tun. Dazu konvertieren sie undefiniertes Verhalten in C oder C ++ in definiertes Verhalten: Absturz des Programms . Null-Dereferenzen und Array-Index-Ausnahmen sollten niemals in einem korrekten C # - oder Java-Programm abgefangen werden. Sie sollten nicht in erster Linie auftreten, da das Programm diesen Fehler nicht haben sollte.
Aber das ist meiner Meinung nach nicht das, was du mit deiner Frage meinst! Wir könnten ganz einfach eine "Deadlock-sichere" Laufzeit schreiben, die regelmäßig überprüft, ob n Threads aufeinander warten, und das Programm beenden, wenn dies passiert, aber ich denke nicht, dass Sie das zufriedenstellen würde.
Welche Mechanismen könnten in eine Programmiersprache implementiert werden, um die Möglichkeit von Rennbedingungen und Deadlocks zu verhindern?
Das nächste Problem, mit dem wir bei Ihrer Frage konfrontiert sind, ist, dass "Rennbedingungen" im Gegensatz zu Deadlocks schwer zu erkennen sind. Denken Sie daran, dass wir bei der Thread-Sicherheit keine Rennen eliminieren wollen . Wir wollen das Programm korrigieren, egal wer das Rennen gewinnt ! Das Problem mit den Rennbedingungen ist nicht, dass zwei Threads in einer undefinierten Reihenfolge ausgeführt werden und wir nicht wissen, wer zuerst fertig wird. Das Problem mit den Rennbedingungen ist, dass Entwickler vergessen, dass einige Reihenfolgen der Thread-Fertigstellung möglich sind, und diese Möglichkeit nicht berücksichtigen.
Ihre Frage lautet also im Wesentlichen: "Gibt es eine Möglichkeit, mit der eine Programmiersprache sicherstellen kann, dass mein Programm korrekt ist?" und die Antwort auf diese Frage lautet in der Praxis nein.
Bisher habe ich nur Ihre Frage kritisiert. Lassen Sie mich hier versuchen, den Gang zu wechseln und den Geist Ihrer Frage anzusprechen. Gibt es Entscheidungen, die Sprachdesigner treffen könnten, um die schreckliche Situation, in der wir uns mit Multithreading befinden, zu mildern?
Die Situation ist wirklich schrecklich! Es ist sehr, sehr schwierig, Multithread-Code korrekt zu machen, insbesondere bei Architekturen mit schwachen Speichermodellen. Es ist lehrreich darüber nachzudenken, warum es schwierig ist:
Es ist also offensichtlich, dass Sprachdesigner die Dinge verbessern können. Geben Sie die Leistungsgewinne moderner Prozessoren auf . Stellen Sie sicher, dass alle Programme, auch Multithread-Programme, ein extrem starkes Speichermodell haben. Dadurch werden Multithread-Programme um ein Vielfaches langsamer, was direkt dem Grund entgegenwirkt, dass Multithread-Programme überhaupt vorhanden sind: für eine verbesserte Leistung.
Selbst wenn man das Speichermodell außer Acht lässt, gibt es noch andere Gründe, warum Multithreading schwierig ist:
Dieser letzte Punkt muss weiter erläutert werden. Mit "zusammensetzbar" meine ich Folgendes:
Angenommen, wir möchten ein int mit einem Double berechnen. Wir schreiben eine korrekte Implementierung der Berechnung:
int F(double x) { correct implementation here }
Angenommen, wir möchten eine Zeichenfolge mit einem int berechnen:
string G(int y) { correct implementation here }
Wenn wir nun eine Zeichenfolge mit einem Double berechnen möchten:
double d = whatever;
string r = G(F(d));
G und F können zusammengesetzt in eine richtige Lösung für das komplexere Problem.
Sperren haben diese Eigenschaft jedoch aufgrund von Deadlocks nicht. Eine korrekte Methode M1, die Sperren in der Reihenfolge L1, L2 akzeptiert, und eine korrekte Methode M2, die Sperren in der Reihenfolge L2, L1 akzeptiert, können nicht beide im selben Programm verwendet werden, ohne ein falsches Programm zu erstellen. Durch Sperren können Sie nicht sagen, dass "jede einzelne Methode korrekt ist, also ist das Ganze korrekt".
Was können wir als Sprachdesigner tun?
Gehen Sie zuerst nicht dorthin. Mehrere Steuerungs-Threads in einem Programm sind eine schlechte Idee, und das Teilen von Speicher zwischen Threads ist eine schlechte Idee. Setzen Sie ihn also nicht in die Sprache oder Laufzeit.
Dies ist anscheinend ein Nichtstarter.
Wenden wir uns dann der grundlegenderen Frage zu: Warum haben wir überhaupt mehrere Threads? Es gibt zwei Hauptgründe, und sie werden häufig zu derselben Sache zusammengeführt, obwohl sie sehr unterschiedlich sind. Sie werden zusammengeführt, weil es bei beiden um die Verwaltung der Latenz geht.
Schlechte Idee. Verwenden Sie stattdessen Single-Threaded-Asynchronität über Coroutinen. C # macht das wunderbar. Java, nicht so gut. Aber dies ist der wichtigste Weg , dass die aktuelle Ernte von Sprachdesignern helfen , das Einfädeln Problem zu lösen. Der await
Operator in C # (inspiriert von asynchronen F # -Workflows und anderem Stand der Technik) wird in immer mehr Sprachen integriert.
Sprachdesigner können helfen, indem sie Sprachfunktionen erstellen, die gut mit Parallelität funktionieren. Denken Sie beispielsweise darüber nach, wie LINQ so natürlich auf PLINQ erweitert wird. Wenn Sie eine vernünftige Person sind und Ihre TPL-Operationen auf CPU-gebundene Operationen beschränken, die sehr parallel sind und keinen gemeinsamen Speicher haben, können Sie hier große Gewinne erzielen.
Was können wir sonst noch tun?
Mit C # können Sie nicht in einem Schloss warten, da dies ein Rezept für Deadlocks ist. Mit C # können Sie keinen Werttyp sperren, da dies immer falsch ist. Sie sperren die Box, nicht den Wert. C # warnt Sie, wenn Sie einen Alias als flüchtig verwenden, da der Alias keine Erfassungs- / Freigabesemantik auferlegt. Es gibt viel mehr Möglichkeiten, wie der Compiler häufig auftretende Probleme erkennen und verhindern kann.
C # und Java haben einen großen Entwurfsfehler gemacht, indem Sie jedes Referenzobjekt als Monitor verwenden konnten. Dies fördert alle Arten von schlechten Praktiken, die es schwieriger machen, Deadlocks aufzuspüren und sie statisch zu verhindern. Und es verschwendet Bytes in jedem Objektheader. Monitore sollten von einer Monitorklasse abgeleitet sein müssen.
STM ist eine schöne Idee, und ich habe in Haskell mit Spielzeugimplementierungen herumgespielt. Sie können damit korrektere Lösungen aus korrekten Teilen viel eleganter zusammenstellen als Lösungen auf Schlossbasis. Ich weiß jedoch nicht genug über die Details, um zu sagen, warum es nicht möglich war, im Maßstab zu arbeiten. Fragen Sie Joe Duffy, wenn Sie ihn das nächste Mal sehen.
Es wurde viel über prozesskalkülbasierte Sprachen geforscht, und ich verstehe diesen Raum nicht sehr gut. Lesen Sie selbst ein paar Artikel darüber und prüfen Sie, ob Sie Einblicke erhalten.
Nachdem ich bei Microsoft an Roslyn gearbeitet hatte, arbeitete ich bei Coverity, und eines der Dinge, die ich tat, war, das Analysator-Frontend mit Roslyn zu bekommen. Durch eine genaue lexikalische, syntaktische und semantische Analyse von Microsoft könnten wir uns dann auf die harte Arbeit konzentrieren, Detektoren zu schreiben, bei denen häufig auftretende Multithreading-Probleme auftreten.
Ein grundlegender Grund, warum wir Rennen und Deadlocks haben und all das Zeug ist, dass wir Programme schreiben, die sagen, was zu tun ist , und es stellt sich heraus, dass wir alle Mist darin sind, imperative Programme zu schreiben. Der Computer tut, was Sie ihm sagen, und wir sagen ihm, dass er die falschen Dinge tun soll. In vielen modernen Programmiersprachen geht es immer mehr um deklarative Programmierung: Sagen Sie, welche Ergebnisse Sie möchten, und lassen Sie den Compiler herausfinden, wie Sie dieses Ergebnis effizient, sicher und korrekt erzielen können. Denken Sie wieder an LINQ. Wir möchten, dass Sie sagen from c in customers select c.FirstName
, was eine Absicht ausdrückt . Lassen Sie den Compiler herausfinden, wie der Code geschrieben wird.
Algorithmen für maschinelles Lernen sind bei einigen Aufgaben viel besser als handcodierte Algorithmen, obwohl es natürlich viele Kompromisse gibt, einschließlich Korrektheit, Zeitaufwand für das Training, Verzerrungen durch schlechtes Training und so weiter. Es ist jedoch wahrscheinlich, dass eine Vielzahl von Aufgaben, die wir derzeit "von Hand" codieren, bald maschinengenerierten Lösungen zugänglich sein werden. Wenn Menschen den Code nicht schreiben, schreiben sie keine Fehler.
Tut mir leid, dass ich dort ein bisschen herumgeschlendert bin. Dies ist ein großes und schwieriges Thema, und in den 20 Jahren, in denen ich Fortschritte in diesem Problembereich verfolgt habe, hat sich in der PL-Community kein klarer Konsens herausgebildet.