Ich habe an vielen Stellen festgestellt, dass es nach kanonischer Weisheit 1 in der Verantwortung des Aufrufers liegt, sicherzustellen, dass Sie sich beim Aktualisieren von UI-Komponenten im UI-Thread befinden (insbesondere in Java Swing, dass Sie sich im Event-Dispatch-Thread befinden ). .
Warum ist das so? Der Event-Dispatch-Thread ist ein Problem der Ansicht in MVC / MVP / MVVM. Wenn Sie es an einer anderen Stelle als in der Ansicht verwenden, wird eine enge Kopplung zwischen der Implementierung der Ansicht und dem Threading-Modell der Implementierung dieser Ansicht hergestellt.
Angenommen, ich habe eine MVC-Architekturanwendung, die Swing verwendet. Wenn der Aufrufer für die Aktualisierung der Komponenten im Event Dispatch-Thread verantwortlich ist und ich versuche, meine Swing View-Implementierung gegen eine JavaFX-Implementierung auszutauschen, muss ich den gesamten Presenter / Controller-Code ändern, um stattdessen den JavaFX-Anwendungsthread zu verwenden .
Ich habe also vermutlich zwei Fragen:
- Warum ist es die Verantwortung des Aufrufers, die Sicherheit des UI-Komponenten-Threads zu gewährleisten? Wo ist der Fehler in meiner Argumentation oben?
- Wie kann ich meine Anwendung so gestalten, dass diese Thread-Sicherheitsbedenken lose verbunden und dennoch angemessen thread-sicher sind?
Lassen Sie mich etwas MCVE-Java-Code hinzufügen, um zu veranschaulichen, was ich unter "verantwortlicher Anrufer" verstehe (hier gibt es einige andere bewährte Methoden, die ich nicht tue, aber ich versuche absichtlich, so minimal wie möglich zu sein):
Anrufer, der verantwortlich ist:
public class Presenter {
private final View;
void updateViewWithNewData(final Data data) {
EventQueue.invokeLater(new Runnable() {
public void run() {
view.setData(data);
}
});
}
}
public class View {
void setData(Data data) {
component.setText(data.getMessage());
}
}
Als verantwortlich ansehen:
public class Presenter {
private final View;
void updateViewWithNewData(final Data data) {
view.setData(data);
}
}
public class View {
void setData(Data data) {
EventQueue.invokeLater(new Runnable() {
public void run() {
component.setText(data.getMessage());
}
});
}
}
1: Der Autor dieses Beitrags hat die höchste Tag-Punktzahl in "Swing on Stack Overflow". Er sagt das überall und ich habe auch gesehen, dass es die Verantwortung des Anrufers auch an anderen Orten ist.
quelle
Antworten:
Gegen Ende seines gescheiterten Traumessays erwähnt Graham Hamilton (ein bedeutender Java-Architekt), dass Entwickler, um "die Gleichwertigkeit mit einem Ereigniswarteschlangenmodell zu wahren, verschiedene nicht offensichtliche Regeln befolgen müssen" und eine sichtbare und explizite Regel haben müssen Ereigniswarteschlangenmodell "scheint Menschen dabei zu helfen, dem Modell zuverlässiger zu folgen und so GUI-Programme zu erstellen, die zuverlässig funktionieren."
Mit anderen Worten, wenn Sie versuchen, eine Multithread-Fassade auf ein Ereigniswarteschlangenmodell zu setzen, tritt die Abstraktion gelegentlich auf nicht offensichtliche Weise auf, die äußerst schwierig zu debuggen sind. Es scheint, als würde es auf dem Papier funktionieren, aber am Ende fällt es in der Produktion auseinander.
Das Hinzufügen kleiner Wrapper um einzelne Komponenten ist wahrscheinlich kein Problem, wie das Aktualisieren eines Fortschrittsbalkens aus einem Arbeitsthread. Wenn Sie versuchen, etwas Komplexeres zu tun, das mehrere Sperren erfordert, wird es sehr schwierig, über die Interaktion zwischen Multithread-Layer und Ereigniswarteschlangen-Layer nachzudenken.
Beachten Sie, dass diese Art von Problemen für alle GUI-Toolkits allgemein gilt. Angenommen, ein Event-Dispatch-Modell in Ihrem Presenter / Controller verbindet Sie nicht eng mit nur einem bestimmten GUI-Toolkit-Parallelitätsmodell, sondern mit allen . Die Schnittstelle für die Ereigniswarteschlange sollte nicht so schwer zu abstrahieren sein.
quelle
Weil das Sicherstellen des GUI-Lib-Threads ein massiver Kopfschmerz und ein Engpass ist.
Der Steuerungsfluss in GUIs verläuft häufig in zwei Richtungen von der Ereigniswarteschlange zum Stammfenster zu den GUI-Widgets und vom Anwendungscode zum Widget, das bis zum Stammfenster übertragen wird.
Es ist schwierig, eine Sperrstrategie zu entwickeln, die das Stammfenster nicht sperrt (was zu vielen Konflikten führt) . Das Sperren von unten nach oben, während ein anderer Thread von oben nach unten sperrt, ist eine großartige Möglichkeit, um einen sofortigen Deadlock zu erzielen.
Und jedes Mal zu überprüfen, ob der aktuelle Thread der GUI-Thread ist, ist kostspielig und kann Verwirrung darüber stiften, was mit der GUI tatsächlich passiert, insbesondere, wenn Sie eine Lese-Update-Schreibsequenz ausführen. Das muss eine Sperre für die Daten haben, um Rennen zu vermeiden.
quelle
Threadedness (in einem Shared-Memory-Modell) ist eine Eigenschaft, die Abstraktionsbemühungen trotzt. Ein einfaches Beispiel ist ein
Set
-Typ: whileContains(..)
undAdd(...)
undUpdate(...)
ist eine perfekt gültige API in einem Szenario mit einem Thread, das Szenario mit mehreren Threads benötigt aAddOrUpdate
.Dasselbe gilt für die Benutzeroberfläche. Wenn Sie eine Liste mit Elementen anzeigen möchten, in der die Anzahl der Elemente über der Liste steht, müssen Sie beide bei jeder Änderung aktualisieren.
Keiner von denen sieht wirklich verlockend aus. Es wird empfohlen, den Moderator für die Verarbeitung des Threadings verantwortlich zu machen, nicht weil es gut ist, sondern weil es funktioniert und die Alternativen schlechter sind.
quelle
System.Windows.Threading.Dispatcher
Versenden sowohl an WPF- als auch an WinForms-UI-Threads erledigen. Diese Art von Ebene zwischen Moderator und Ansicht ist auf jeden Fall nützlich. Es bietet jedoch nur Toolkit-Unabhängigkeit, keine Threading-Unabhängigkeit.Ich habe vor einiger Zeit einen wirklich guten Blog gelesen, der dieses Thema behandelt (von Karl Bielefeldt erwähnt). Grundsätzlich heißt es, dass es sehr gefährlich ist, den Thread des UI-Kits sicher zu machen, da er mögliche Deadlocks einführt und davon abhängt, wie es ist umgesetzt, Rennbedingungen in den Rahmen.
Es gibt auch eine Leistungsüberlegung. Nicht so sehr jetzt, aber als Swing zum ersten Mal veröffentlicht wurde, wurde es heftig für seine Leistung kritisiert (war schlecht), aber das war nicht wirklich Swings Schuld, es war das mangelnde Wissen der Leute darüber, wie man es benutzt.
SWT erzwingt das Konzept der Threadsicherheit, indem es Ausnahmen auslöst, wenn Sie nicht hübsch dagegen verstoßen, aber zumindest darauf aufmerksam gemacht werden.
Wenn Sie sich zum Beispiel den Malprozess ansehen, ist die Reihenfolge, in der die Elemente gemalt werden, sehr wichtig. Sie möchten nicht, dass das Malen einer Komponente einen Nebeneffekt auf einen anderen Teil des Bildschirms hat. Stellen Sie sich vor, Sie könnten die Texteigenschaft einer Beschriftung aktualisieren, aber sie wurde von zwei verschiedenen Threads gezeichnet. Dies könnte zu einer beschädigten Ausgabe führen. Alle Malvorgänge werden also in einem einzigen Thread ausgeführt, normalerweise basierend auf der Reihenfolge der Anforderungen / Anforderungen (aber manchmal verdichtet, um die Anzahl der tatsächlichen physischen Malzyklen zu reduzieren).
Sie erwähnen den Wechsel von Swing zu JavaFX, aber Sie hätten dieses Problem mit so gut wie jedem UI-Framework (nicht nur mit Thick Clients, sondern auch mit Web). Swing scheint das Problem hervorzuheben.
Sie könnten eine Zwischenschicht (einen Controller eines Controllers?) Entwerfen, deren Aufgabe es ist, sicherzustellen, dass Aufrufe an die Benutzeroberfläche korrekt synchronisiert werden. Es ist unmöglich, genau zu wissen, wie Sie Ihre Nicht-UI-Teile Ihrer API aus der Perspektive der UI-API entwerfen könnten, und die meisten Entwickler würden sich darüber beschweren, dass ein in die UI-API implementierter Thread-Schutz zu restriktiv war oder nicht ihren Anforderungen entsprach. Besser, Sie können entscheiden, wie Sie dieses Problem lösen möchten, basierend auf Ihren Anforderungen
Eines der größten Probleme, das Sie berücksichtigen müssen, ist die Möglichkeit, eine bestimmte Reihenfolge von Ereignissen auf der Grundlage bekannter Eingaben zu rechtfertigen. Wenn der Benutzer beispielsweise die Größe des Fensters ändert und das Ereigniswarteschlangenmodell garantiert, dass eine bestimmte Reihenfolge von Ereignissen auftritt, scheint dies einfach zu sein. Wenn die Warteschlange jedoch zulässt, dass Ereignisse von anderen Threads ausgelöst werden, kann die Reihenfolge in nicht mehr garantiert werden Wenn die Ereignisse eintreten könnten (eine Racebedingung), muss man sich plötzlich Sorgen über verschiedene Zustände machen und nichts tun, bis etwas anderes passiert und man beginnt, Zustandsflaggen zu teilen, und am Ende bekommt man Spaghetti.
Okay, Sie könnten das lösen, indem Sie eine Art Warteschlange haben, die die Ereignisse nach dem Zeitpunkt ihrer Ausgabe anordnet. Aber haben wir das nicht schon? Außerdem können Sie immer noch nicht garantieren, dass Thread B seine Ereignisse NACH Thread A generiert
Der Hauptgrund, warum Menschen sich darüber aufregen, über ihren Code nachdenken zu müssen, ist, dass sie gezwungen sind, über ihren Code / ihr Design nachzudenken. "Warum kann es nicht einfacher sein?" Einfacher geht es nicht, denn es ist kein einfaches Problem.
Ich erinnere mich, als die PS3 herausgebracht wurde und Sony den Cell-Prozessor auf den neuesten Stand brachte. Er ist in der Lage, verschiedene Logikzeilen auszuführen, Audio- und Videodaten zu dekodieren, Modelldaten zu laden und aufzulösen. Ein Spieleentwickler fragte: "Das ist alles großartig, aber wie synchronisiert man die Streams?"
Das Problem, von dem der Entwickler sprach, war, dass all diese separaten Streams zu einem bestimmten Zeitpunkt für die Ausgabe auf eine einzelne Pipe synchronisiert werden mussten. Der arme Moderator zuckte nur mit den Achseln, da es kein Thema war, mit dem sie vertraut waren. Offensichtlich hatten sie Lösungen, um dieses Problem zu lösen, aber es war zu der Zeit lustig.
Moderne Computer nehmen eine Menge Eingaben von vielen verschiedenen Orten gleichzeitig auf. Alle Eingaben müssen verarbeitet und an den Benutzer weitergeleitet werden, ohne dass die Darstellung anderer Informationen beeinträchtigt wird. Es handelt sich also um ein komplexes Problem eine einfache Lösung.
Da Sie nun die Möglichkeit haben, Frameworks zu wechseln, ist dies nicht einfach zu entwerfen, ABER nehmen Sie sich einen Moment Zeit für MVC. MVC kann mehrschichtig sein, dh Sie könnten eine MVC haben, die sich direkt mit der Verwaltung des UI-Frameworks befasst könnte dies dann wieder in eine übergeordnete MVC-Ebene einschließen, die sich mit Interaktionen mit anderen (möglicherweise multi-threading) Frameworks befasst. Es wäre die Verantwortung dieser Ebene, zu bestimmen, wie die untergeordnete MVC-Ebene benachrichtigt / aktualisiert wird.
Anschließend verwenden Sie die Codierung, um Entwurfsmuster und Factory- oder Builder-Muster für die Erstellung dieser verschiedenen Ebenen zu verknüpfen. Dies bedeutet, dass Sie Multithread-Frameworks durch die Verwendung einer mittleren Ebene als Idee von der UI-Ebene entkoppelt werden.
quelle