Start - Publikationen - Wissen - TOGAF - Impressum -

Nebenläufige Verarbeitung mit Swing


AWT und Swing sind grundsätzlich single-threaded konzipiert. An keiner (für den Programmierer relevanten) Stelle des GUI Frameworks wird synchronisierter Code verwendet und es wird vorausgesetzt, dass der GUI Programmierer auf synchronisierten Code verzichtet. Dieses Prinzip vereinfacht die Nutzung des Frameworks erheblich und befreit den GUI Programmierer von der Last der Formulierung und Kontrolle nebenläufiger Verarbeitung. Dennoch leistet das Framework die wirksame Illusion einer nebenläufigen Verarbeitung, das ist auch notwendig, da eine grafische Oberfläche eine Menge Ereignisse erzeugt und scheinbar parallel verarbeitet. So kann man die Maus bewegen (eine Unmenge von Mouse-Move Events wird generiert) und gleichzeitig korrekt eine Texteingabe durchführen, dabei werden ständig alle sichtbaren GUI Elemente neu gezeichnet.

In diesem Artikel wird erklärt, welches Architekturmuster dieses Verhalten umsetzt und welche Implikationen für die professionelle GUI Programmierung daraus resultieren.

Die Event Dispatching Queue


Eine sichtbare, eingabebereite Swing Anwendung wird durch einen Event Dispatching Thread repräsentiert, der Jobs einer Event Dispatching Queue (EDQ) abarbeitet. Diese Jobs beinhalten im Wesentlichen:

  1. Die Abarbeitung von Action Events, die durch Nutzereingaben wie das Bewegen und Klicken mit der Maus, Änderung des Fokus und Tastatureingaben erzeugt und auf die EDQ gelegt werden.
  2. Das Neuzeichnen aller sichtbaren und geänderten Teile der Nutzeroberfläche. Das geschieht mindestens all 50 ms, damit der Eindruck einer flimmer- und ruckelfreien Anwendung entsteht.
Das korrekte Ablegen dieser Jobs auf die EDQ wird vom Swing Framework automatisch erledigt. Dieses Architekturmuster ist weit verbreitet. Beispielsweise jede Android GUI oder die Verarbeitung von JavaScript Code in einem Node.js Kontainer erfolgt nach genau diesem Muster und entlastet den Programmierer entscheidend von der Aufgabe der korrekten Organisation nebenläufiger Abarbeitung.

Implikation 1: Umgang mit langsamen oder blockierenden Code


Die wichtigste Grundregel im Rahmen des beschriebenen Architekturmusters lautet demnach: der Event Dispatching Thread darf nicht bei seiner Abarbeitung der Jobs der EDQ gestört (verzögert) werden. Das bedeutet: länger laufende Action Event Verabreitungen und Zeichenaktionen müssen in einem eigenen Thread ausgelagert werden, damit die Kontrolle zur Weiterverarbeitung der EDQ wieder schnell zurück an den Event Dispatching Thread geht. Ein sinnvolles Maß dafür was "schnell" ist, geben die 50 ms mit der optimalerweise das Neuzeichnen der Oberfläche geschehen sollte.

Gründe für die Verzögerung können sein:

  1. eine Action erledigt eine rechenintensive Auswertung (CPU Last)
  2. eine Action greift blockierend auf externe Ressourcen wie das lokale Filesystem oder eine Netzwerkressource zu, insbesondere zählen dazu die Funktionsaufrufe zentraler Services
  3. das Zeichnen einer Oberflächenkomponente ist aufwändig, beispielsweise weil der Funktionsgraph einer komplizierten mathematischen Formel, eine komplexe Szenerie oder eine rekursive Grafik gezeichnet werden soll
  4. das Zeichnen einer Oberflächenkomponente hängt vom Zustand eines sich langsam ändernden Status ab (wie es beispielsweise bei der Implementierung eines Fortschrittsbalkens der Fall ist)
Swing unternimmt leider keinerlei Maßnahmen, um eine Verzögerung des Event Dispatching Threads zu ermitteln und dem Programmierer als Warnmeldung zu präsentieren. Ein solches Verhalten führte dazu, dass eine ganze Menge schlechter Swing Code niemals ausgeliefert worden wäre und Swing wohl nicht den Ruf als "langsam" bekommen hätte. Hier sind moderne GUI Framework (wie Android) konsequenter und erlauben sogar das Abbrechen der gesamten Anwendung, wenn diese nicht mehr reaktiv ist.

So schwerwiegend die Effekte langsamer Abarbeitung durch den Event Dispatching Thread auch sind, die Lösung des Problems ist sehr einfach: die Kandidaten langsamer oder blockierender Abarbeitungen werden identifiziert und der entsprechenden Code einfach in der run-Methode eines eigenen Threads ausgeführt.

Implikation 2: Präsentation der Ergebnisse


Irgendwann liegt das Ergebnis der Abarbeitung eines nebenläufigen Threads vor und soll präsentiert werden. Dafür darf keinesfalls aus diesem Threads heraus ein Zugriff auf die vom Event Dispatching Thread verwalteten Ressourcen erfolgen, denn diese Ressourcen sind nicht für eine nebenläufige Abarbeitung entworfen. Versucht man es doch, wird man von Swing nicht gewarnt und riskiert, dass die gesamte Anwendung in einen undefinierten Zustand gerät. Auch an dieser Stelle sind modernere GUI Frameworks weiter und verhindern Zugriffe von Threads, die nicht durch das Framework verwaltet werden auf die Ressourcen des Frameworks mit einer Fehlermeldung.

Die korrekte Präsentation der Ergebnisse nebenläufiger Abarbeitung erfolgt grundsätzlich über die Ablage eines Jobs auf der EDQ. Dafür gibt es im beschriebenen Architekturmuster ein entsprechendes Programmieridiom das man kennen und einhalten muss. In Swing nutzt man die Methode SwingUtilities.invokeLater für diese Aufgabe:

SwingUtilities.invokeLater(new Runnable() {
  @Override
  public void run() {
    // publiziere das Ergebnis einer langen Abarbeitung:
    // dieser Code wird vom Event Dispatching Thread ausgeführt und
    // hier darf der Zustand von GUI Elementen geändert werden
  }
});

Eine andere Hilfsmethode der SwingUtilities.invokeAndWait legt ebenfalls einen Job auf die EDQ und blockiert, bis dieser Job abgearbeitet ist:

SwingUtilities.invokeAndWait(new Runnable() {
  @Override
  public void run() {
    // dieser Code wird vom Event Dispatching Thread ausgeführt
  }
});
// blockiert, bis der Job abegarbeitet ist: der nachfolgende Code 
// wird also erst ausgeführt, wenn die run-Methode des Runnable
// durch den Event Dispatching Thread abgearbeitet wurde
... 

Ruft man SwingUtilities.invokeAndWait vom Event Dispatching Thread, fällt dieser in ein Deadlock. invokeAndWait muss also zwingend von einem nebenläufigen Thread gerufen werden. Ein wichtiger Anwendungsfall für invokeAndWait betrifft übrigens das korrekte Starten eines Swing-GUI:

public static void main(String[] args) {
  ...
  // initialisiere alles, was nicht mit der GUI zu tun hat  
  ...
  SwingUtilities.invokeAndWait(new Runnable() {
    @Override
    public void run() {
      buildAndShowGUI();
    }
  });
  // wenn es hier weiter geht, ist das GUI initialisiert und der 
  // Event Dispatching Thread übernimmt die Regie, der main-Thread kann nun 
  // sterben
}
Verzichten Sie auf diesen Konstrukt kann es passieren, dass der main-Thread stirbt, bevor der Event Dispatching Thread seine Arbeit aufgenommen hat (dann wird das GUI nicht starten). Das Resultat ist abhängig von der Umgebung - kann zum Beispiel in der Entwicklung noch funktionieren, in Produktion aber versagen.

Nebenläufigkeit und MVC


Im MVC Artikel ist die Wichtigkeit der Trennung der Fachdaten vom Rest des GUI herausgestellt worden. Diese Sicht kann nun erweitert werden, indem man von Anfang an plant, die Bewirtschaftung der Fachdaten konsequent außerhalb des Event Dispatching Thread zu organisieren.

                       +----------+
view implements    --> |  Fach    | 
 IModelListener        |  Daten   |
                       |    +     |
control implements --> |  IModel  |
 IModelListener        | Listener |
                       +----------+

In der Abbildung haben wir oberhalb der roten Linie die "GUI-Welt", die ausschließlich vom Event Dispatching Thread kontrolliert werden soll und unterhalb das model, das sind die Fachdaten, die potenziell auch über nebenläufige Worker-Threads bewirtschaftet werden. Die Implementierung der roten Trennlinie zwischen beiden Welten lässt sich folgendermaßen charakterisieren:

Kommunikationsweg Implementierung
control oder view (also die Viewmodell-Implementierungen) lesen im model (grauer Pfeil rechts und links) Im Kontext des Event Dispatching Thread werden Inhalte der Fachdaten gelesen. Die dafür zuständigen Methoden des model sind geeignet synchronisiert und dürfen niemals blockieren.
control ändert im model (grauer Pfeil rechts) Ausgelöst durch Nutzerinteraktion soll eine Änderung im model erfolgen. Nicht blockierende Änderungen werden dabei einfach direkt (im Event Dispatching Thread) erledigt. Für blockierende Änderungen startet der Event Dispatching Thread einen Workerthread, in dem diese Abarbeitung erledigt wird. Die Wirkung einer solchen Änderung (und die Manifestationen in der GUI) wird über die Implementierung von Model-Listener sichergestellt (blaue Pfeile, siehe nächste Tabellenzeile).
model beanchrichtigt control und view (blaue Pfeile) control und view (also die Viewmodell-Implementierungen) haben sich als Model-Listener im model angemeldet und werden nun benachrichtigt. Das erfolgt im Kontext eines Workerthreads, die Implementierung der Model-Listener-Logik wird deshalb in ein SwingUtilities.invokeLater gekapselt und somit die Kontrolle wieder an den Event Dispatching Thread übergeben.

Die Implementierungsrezepte für die beiden letzten Zeilen der Tabelle sind universell anwendbar und sehr wirksam. Die erste Zeile in der Tabelle kann im richtigen Leben eine harte Nuss sein (wie immer, wenn Daten parallel verarbeitet und dennoch konsistent bereitgestellt werden sollen).

Beispiel: Implementierung eines Wachhundes


Da Swing nicht über verzögerte Abarbeitung der EDQ informiert, sollte eine entsprechende Wachhund-Funktion mit eigenen Mitteln bereitgestellt werden. Dazu wird einfach ein ewig laufender Thread gestartet, der mit SwingUtilities.invokeAndWait die Differenz zweier EDQ-Durchläufe misst:

Thread watchDog = new Thread() {
  @Override
  public void run() {
    long time = System.currentTimeMillis();
    for (;;) { // runs forever
      try {
        SwingUtilities.invokeAndWait(new Runnable() {
          @Override
          public void run() {
            // just wait, do nothing
          }
        });
      } catch (Exception e) {
        // well...
      }
      long delta = time;
      time = System.currentTimeMillis();
      delta = time - delta;
      if (delta > 100) {
        System.err.println("WARNING! EDT is frustrated, some action or painting takes too long: " + delta + " ms for one loop");
      }
    }
  };
};
watchDog.start();

Beispiel: Implementierung einer komplexen Grafik


Wie erläutert darf das Zeichnen komplexer Diagramme nicht in der paint Methode der betreffenden Komponente implementiert sein, da ein Aufruf von paint Teil der Abarbeitungssequenz des Event Dispatching Threads ist. Hier kommt das Programmiermuster des double buffered painting zum Einsatz. Das Grundprinzip dabei besteht darin, dass eine GUI Komponente zwei Images bereit hält: ein Image wird in paint zur Anzeige gebracht, das andere Image (off screen image) wird nebenläufigen Threads zum Zeichnen zur Verfügung gestellt. Ist ein nebenläufiger Thread fertig mit dem Zeichnen des off screen images, dann signalisiert er ein "Umschalten" und die Rolle der beiden Images wird vertauscht. Eine beispielhafte Implementierung einer solchen Komponente zeigt auch sehr schön, wie die Zugriffe zum Ändern und Auslesen des Zustands der Komponente über serielle Abarbeitung auf der EDQ "synchronisiert" werden:

public class DiagramPane extends JComponent {
  //
  private static final long serialVersionUID = 4740170443328960918L;
  //
  // zwei Images, eines zum Zeichnen, eines zum Anzeigen
  private BufferedImage halbbild0, halbbild1;
  // Schalter zwischen beiden Zuständen
  private boolean flipflop = true;
  //
  // muss vom main-Thread im Rahmen der Initialisierung der Komponente gerufen werden,
  // sobald der grafische Kontext zur Verfügung steht
  public void init(final int width, final int height) throws Exception {
    SwingUtilities.invokeAndWait(new Runnable() {
      public void run() {
        GraphicsConfiguration gfxConf = GraphicsEnvironment
	  .getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
        halbbild0 = gfxConf.createCompatibleImage(width, height);
        halbbild1 = gfxConf.createCompatibleImage(width, height);
      }
    });
  }
  //
  @Override 
  public void paint(Graphics g) {
    // hier wird einfach nur der Inhalt des gerade nicht
    // in Bearbeitung befindlichen Images angezeigt
    g.drawImage(getFlipflop() ? halbbild0 : halbbild1, 0, 0, this);
  }
  //
  private boolean getFlipflop() {
    return this.flipflop;
  }
  // Ändert das aktive Image dieser Komponente und gibt das inaktive zum
  // Zeichnen zurück, diese Methode wird nebenläufig gerufen
  public BufferedImage flip() {
    class ImageHolder {
      BufferedImage offscreenImage = null;
    }
    final ImageHolder imageHolder = new ImageHolder();
    try {
      SwingUtilities.invokeAndWait(new Runnable() {
        public void run() {
          // ändere die Rolle von aktiven und inaktivem Image
          DiagramPane.this.flipflop = !DiagramPane.this.flipflop;
          // markiere die Komponente zum Neuzeichnen
          DiagramPane.this.repaint();
          // besorge das inaktive Halbbild
          imageHolder.offscreenImage = getFlipflop() ? halbbild1 : halbbild0;
        }
      });
    } catch (Exception e) {
      e.printStackTrace();
    } 
    return imageHolder.offscreenImage;
  }
}
In einem nebenläufigen Kontext kann nun in einer Endlosschleife das Zeichnen Abseits der Aufgaben des Event Dispatching Threads erfolgen:
for (;;) { // forever
  // blockiert, bis der EDT das aktive Image umgeschaltet hat
  BufferedImage image = diagramPane.flip(); 
  // Zeichne auf dem off screen image
  Graphics g = image.getGraphics();
  g.clearRect(0, 0, width, height);
  // 
  // viele komplizierte und lang dauernde Zeichenoperationen
  ...
}
Hinweis: diese Beispielimplementierung darf aber nicht von mehreren nebenläufigen Threads genutzt werden.

Implikation 3: Exception Handling


Wie beschrieben wird der Programmfluss (eines gestarteten und fertig initialisierten) Swing-Clients repräsentiert durch die Abarbeitung einer Event Queue. Treten bei der Abarbeitung Unchecked Exceptions auf, wird das betreffende Event nicht zu Ende abgearbeitet und eine Standardfehlerbehandlung erfolgt. Professionelles Logging muss dieses Standardverhalten beeinflussen. Dazu muss man eine System-Property namens sun.awt.exception.handler bereitstellen, die den FQN einer selbstimplementierten Handlerklasse enthält.

// Logging für uncaught exceptions aus dem AWT-EventHandling
System.getProperties().put("sun.awt.exception.handler", "com.foo.bar.MyAwtExceptionHandler"); 
Die Handlerklasse braucht kein Java-Interface zu implementieren. Allerdings muss sie eine Methode mit der Signatur
void handle(Throwable e)
enthalten. Diese Methode wird dann im Fall einer unbehandelten Exception vom AWT-Thread aufgerufen.

Referenzen


Lesson: Concurrency in Swing
Multithreaded toolkits: A failed dream?

copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved