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.
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:
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:
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.
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.
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.
Lesson: Concurrency in Swing
Multithreaded toolkits: A failed dream?