Warum ist der Aufrufer für die Gewährleistung der Thread-Sicherheit bei der GUI-Programmierung verantwortlich?

37

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:

  1. Warum ist es die Verantwortung des Aufrufers, die Sicherheit des UI-Komponenten-Threads zu gewährleisten? Wo ist der Fehler in meiner Argumentation oben?
  2. 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.

durron597
quelle
1
Eh, Leistung IMHO. Diese Event-Postings sind nicht kostenlos und in nicht-trivialen Apps möchten Sie die Anzahl minimieren (und sicherstellen, dass keine zu groß ist), aber die Minimierung / Verdichtung sollte logischerweise im Presenter erfolgen.
Ordous
1
@Ordous Sie können weiterhin sicherstellen, dass die Buchungen minimal sind, während Sie die Übergabe des Threads in die Ansicht stellen.
Durron597
2
Ich habe vor einiger Zeit einen wirklich guten Blog gelesen, in dem dieses Problem diskutiert wird. 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 je nach Implementierung die Race-Bedingungen 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 der Mangel an Wissen, wie man es benutzt.
MadProgrammer
1
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. Sie erwähnen den Wechsel von Swing zu JavaFX, aber Sie haben dieses Problem mit so gut wie jedem UI-Framework. Swing scheint nur das zu sein, das das Problem hervorhebt. 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
MadProgrammer
1
und die meisten Entwickler würden sich darüber beschweren, dass jeglicher Thread-Schutz, der in der UI-API implementiert ist, zu restriktiv ist oder nicht ihren Anforderungen entspricht. Besser, Sie können entscheiden, wie Sie dieses Problem lösen möchten, basierend auf Ihren Anforderungen
MadProgrammer

Antworten:

22

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.

Karl Bielefeldt
quelle
25

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.

Ratschenfreak
quelle
1
Interessanterweise löse ich dieses Problem, indem ich ein Warteschlangen-Framework für diese von mir geschriebenen Updates habe, das die Übergabe des Threads verwaltet.
Durron597
2
@ durron597: Und du hast nie irgendwelche Updates, die abhängig vom aktuellen Zustand der Benutzeroberfläche einen anderen Thread beeinflussen könnten? Dann könnte es klappen.
Deduplicator
Warum benötigen Sie verschachtelte Sperren? Warum müssen Sie das gesamte Stammfenster sperren, wenn Sie an Details in einem untergeordneten Fenster arbeiten? Lock ist , um ein lösbares Problem durch ein einziges Verfahren ausgesetzt Bereitstellung mutliple Schlösser in der richtigen Reihenfolge (entweder von oben nach unten oder von unten nach oben, aber nicht verlassen , die Wahl für den Anrufer) zu sperren
MSalters
1
@MSalters mit root meinte ich das aktuelle Fenster. Um alle Sperren zu erhalten, die Sie erwerben müssen, müssen Sie die Hierarchie durchlaufen, die das Sperren jedes Containers erfordert, wenn Sie auf den übergeordneten Container stoßen, und das Entsperren (um sicherzustellen, dass Sie nur von oben nach unten sperren). Hoffen Sie dann, dass sich daran nichts geändert hat Sie, nachdem Sie das Stammfenster erhalten und Sie die Verriegelung von oben nach unten tun.
Ratschenfreak
@ratchetfreak: Wenn das Kind, das Sie sperren möchten, von einem anderen Thread entfernt wird, während Sie es sperren, ist dies nur ein bisschen bedauerlich, aber für das Sperren nicht relevant. Sie können ein Objekt, das gerade von einem anderen Thread entfernt wurde, nicht bearbeiten. Aber warum entfernt dieser andere Thread Objekte / Fenster, die Ihr Thread noch verwendet? Das ist in keinem Szenario gut, nicht nur in der Benutzeroberfläche.
MSalters
17

Threadedness (in einem Shared-Memory-Modell) ist eine Eigenschaft, die Abstraktionsbemühungen trotzt. Ein einfaches Beispiel ist ein Set-Typ: while Contains(..)und Add(...)und Update(...)ist eine perfekt gültige API in einem Szenario mit einem Thread, das Szenario mit mehreren Threads benötigt a AddOrUpdate.

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.

  1. Das Toolkit kann dieses Problem nicht lösen, da das Sperren nicht sicherstellt, dass die Reihenfolge der Vorgänge korrekt bleibt.
  2. Die Ansicht kann das Problem lösen, aber nur, wenn Sie der Geschäftsregel erlauben, dass die Nummer oben in der Liste mit der Anzahl der Elemente in der Liste übereinstimmen muss und die Liste nur über die Ansicht aktualisiert wird. Nicht genau das, was MVC sein soll.
  3. Der Moderator kann das Problem lösen, muss sich jedoch darüber im Klaren sein, dass die Ansicht spezielle Anforderungen in Bezug auf das Threading hat.
  4. Eine weitere Option ist die Datenbindung an ein Multithreading-fähiges Modell. Dies erschwert das Modell jedoch mit Dingen, die für die Benutzeroberfläche von Belang sein sollten.

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.

Patrick
quelle
Möglicherweise könnte eine Ebene zwischen der Ansicht und dem Präsentator eingefügt werden, die auch ausgetauscht werden könnte.
Durron597
2
.NET muss das System.Windows.Threading.DispatcherVersenden 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.
Patrick
9

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.

MadProgrammer
quelle
2
Mit dem Web hätten Sie dieses Problem nicht. JavaScript hat bewusst keine Threading-Unterstützung - eine JavaScript-Laufzeit ist praktisch nur eine große Ereigniswarteschlange. (Ja, ich weiß, dass JS genau genommen WebWorker hat - das sind verfälschte Threads, die sich eher wie Schauspieler in anderen Sprachen verhalten).
James_pic
1
@James_pic Was Sie tatsächlich haben, ist der Teil des Browsers, der als Synchronisator für die Ereigniswarteschlange fungiert. Dies ist im Grunde das, worüber wir gesprochen haben. Der Anrufer war dafür verantwortlich, sicherzustellen, dass Aktualisierungen in der Ereigniswarteschlange des Toolkits erfolgen
MadProgrammer,
Ja genau. Der Hauptunterschied im Web besteht darin, dass diese Synchronisierung unabhängig vom Code des Aufrufers erfolgt, da die Laufzeit keinen Mechanismus bietet, der die Codeausführung außerhalb der Ereigniswarteschlange ermöglicht. Der Anrufer muss also keine Verantwortung dafür übernehmen. Ich glaube, das ist auch ein großer Teil der Motivation für die Entwicklung von NodeJS. Wenn die Laufzeitumgebung eine Ereignisschleife ist, ist der gesamte Code standardmäßig für Ereignisschleifen ausgelegt.
James_pic
1
Das heißt, es gibt Browser-UI-Frameworks, die ihre eigene Ereignisschleife innerhalb einer Ereignisschleife haben (ich sehe Sie als Angular an). Da diese Frameworks von der Laufzeit nicht mehr geschützt werden, müssen Aufrufer auch sicherstellen, dass Code innerhalb der Ereignisschleife ausgeführt wird, ähnlich wie bei Multithread-Code in anderen Frameworks.
James_pic
Gibt es ein besonderes Problem, wenn Steuerelemente, die nur angezeigt werden, automatisch die Threadsicherheit gewährleisten? Wenn ein bearbeitbares Textsteuerelement zufällig von einem Thread geschrieben und von einem anderen gelesen wird, gibt es keine gute Möglichkeit, das Schreiben für den Thread sichtbar zu machen, ohne mindestens eine dieser Aktionen mit dem tatsächlichen UI-Status des Steuerelements zu synchronisieren. Aber wären solche Probleme für Nur-Anzeige-Steuerelemente von Bedeutung? Ich würde denken, dass diese leicht alle erforderlichen Sperren oder Verriegelungen für Operationen bereitstellen können, die an ihnen ausgeführt werden, und es dem Anrufer ermöglichen, den Zeitpunkt zu ignorieren, zu dem ...
supercat