ProgressBars, Algorithmen und Multithreading

Aus Byte-Welt Wiki
Zur Navigation springenZur Suche springen

Manchmal schreibt man eine Methode, die "etwas mehr Zeit" benötigt. Wenn eine Methode länger als 3 Sekunden das Programm blockiert, ist ein Fortschrittsbalken eine gute Idee, den User bei Laune zu halten.

(Die menschliche Aufmerksamkeitsspanne ist etwa 3 Sekunden. Was länger als 3 Sekunden still steht, wird als unbeweglich wahrgenommen. Im Falle eines Programmes bedeutet dies: "Das Programm ist voll schlecht, das arbeitet ja gar nicht!"

Mit einem Fortschrittsbalken lenkt man den Benutzer ab. Der Algorithmus der ausgeführt wird, ist dadurch nicht schneller, im Gegenteil, der Fortschrittsbalken benötigt zusätzliche Rechenzeit. Objektiv verlangsamt man also das Programm, aber subjektiv wird es schneller. Und Menschen sind nun einmal grausam subjektive Wesen...)

Für den Fortschrittsbalken wird die Komponente JProgressBar verwendet. Der Algorithmus wird in mehr oder weniger regelmäßigen Abständen den Wert (value) des Balkens verändern.

Es stellt sich nun das Problem, dass gleichzeitig ein Algorithmus ablaufen, und ein Fortschittsbalken gezeichnet werden soll. Man kann dies mit Multithreading, also dem Aufteilen des Programmes in mehrere Prozesse, erreichen.

Da der Aufbau von Swing (bzw. AWT) es nicht erlaubt, Teile der graphischen Oberfläche in einen anderen Thread auszulagern, muss man in jedem Fall den Algorithmus auslagern. Und da Swing nicht threadsicher ist, darf man von dem ausgelagerten Algorithmus eigentlich nicht auf den Fortschrittsbalken zugreifen.

Glücklicherweise haben die Swing-Programmierer, extra um dieses Problem zu lösen, die Methoden SwingUtilities.invokeLater() und SwingUtilities.invokeAndWait() geschrieben. Diese Methoden nehmen eine Instanz eines Runnable's auf, und speichern es vorerst. Zu einem späteren Zeitpunkt wird der EventDispatcher-Thread (der Thread, der die graphische Oberfläche zeichnet und Events herumschickt) dieses Runnable nehmen und die Methode run() ausführen.

Der Unterschied zwischen invokeLater() und invokeAndWait()

Bei einem Aufruf von invokeLater() kann der aufrufende Thread sofort weiterarbeiten. Bei einem Aufruf von invokeAndWait() wartet der aufrufende Thread bis das Runnable abgearbeitet wurde. invokeAndWait() verhält sich also ganz ähnlich, als würde man einen gigantischen synchronized-Block über die GUI legen und in diesem Block irgendwas berechnen. Für die run()-Methode des Runnables gilt dasselbe wie für alle anderen Methoden, die im EventDispatcher ausgeführt werden: sie muss schnell abgearbeitet werden, denn sonst friert die Oberfläche ein.

Übrigens: invokeAndWait() sollte man auf keinen Fall in einer Methode wie actionPerformed() (oder jede andere Methode, die vom EventDispatcher aufgerufen wird) benutzen. Das Resultat ist stets eine Exception, oder noch schlimmer, ein eingefrorenes Programm.

Es sei auch auf die Möglichkeit der Verwendung eines ProgressMonitors hingewiesen, welcher das Implementieren eines Frames/Dialoges mit dem Fortschrittsbalken überflüssig macht.

Das folgende Beispiel zeigt ein Fenster, auf dem ein Fortschrittsbalken und zwei Knöpfe sind. Mit dem einen - start - wird ein Algorithmus gestartet, der Fortschrittsbalken beginnt zu wandern. Mit dem zweiten - stop - wird der Algorithmus abgebrochen, alles kehrt wieder zum Anfangszustand zurück (Den Stop-Button könnte man auch in einen Pause-Button umwandeln ).

import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
/**
 * Ein hübsches Fenster mit einem Fortschrittsbalken und zwei Knöpfen "start"
 * und "stop".
 */
public class Main extends JFrame implements Runnable{   
    /** Der Algorithmus, der ausgeführt werden soll */
    private Algorithmus algorithmus;
    /** Der Fortschrittsbalken */
    private JProgressBar progress;
    /** Verschiedene Knöpfe um den Algorithmus zu steuern */
    private JButton buttonStart, buttonStop;
    /**
     * Irgendwo muss das Programm ja starten...
     * @param args Zusätzliche Argumente, dieser Wert wird zurzeit nicht benötigt
     */
    public static void main( String[] args ){
        Main main = new Main();
        main.setVisible( true );
    }
    /**
     * Standartkonstruktor, stellt ein neues Fenster her.
     */
    public Main(){
        setTitle( "Progress-Demo" );
        progress = new JProgressBar();
        buttonStart = new JButton( "Start" );
        buttonStop = new JButton( "Stop" );
        algorithmus = new Algorithmus(); // Wir brauchen ja einen Algorithmus ;-)
        // Wenn der Algo nicht läuft, kann man ihn auch nicht stoppen
        buttonStop.setEnabled( false ); 
        // Der folgende Teil ist nur für das Auge, eine Benutzeroberfläche die
        // etwas dynamisch ist, kommt immer gut an.
        Container content = getContentPane();
        content.setLayout( new GridBagLayout() );
        JPanel panel = new JPanel( new GridLayout( 1, 2 ));
        panel.add( buttonStart );
        panel.add( buttonStop );
        content.add( progress, new GridBagConstraints( 0, 0, 1, 1, 1.0, 1.0, 
                GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, 
                new Insets( 2, 2, 2, 2 ), 0, 0) );
        content.add( panel, new GridBagConstraints( 0, 1, 1, 1, 1.0, 1.0, 
                GridBagConstraints.SOUTHEAST, GridBagConstraints.NONE, 
                new Insets( 2, 2, 2, 2 ), 0, 0) );      
        // Die Buttons sollen ja auch reagieren können
        buttonStart.addActionListener( new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                start();
            }
        });
        buttonStop.addActionListener( new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                stop();
            }
        });
        // Geeignete Grösse und Position wählen
        pack();
        setLocationRelativeTo( null );
        // Eigentlich müsste man ja einen WindowListener implementieren, und
        // den Algorithmus korrekt beenden, aber wir schiessen einfach das
        // ganze Programm ab
        setDefaultCloseOperation( EXIT_ON_CLOSE );
    }
    /**
     * Startet den Algorithmus, das Verhalten ist nicht definiert, wenn der
     * Algorithmus bereits aktiviert wurde.
     */
    protected void start(){
        buttonStart.setEnabled( false );
        // Einen neuen Thread herstellen, und starten.
        new Thread( this ).start();
        buttonStop.setEnabled( true );
    }
    /**
     * Stoppt den Algorithmus, immer unter der Annahme, dass er zurzeit
     * läuft.
     */
    protected void stop(){
        buttonStop.setEnabled( false );
        algorithmus.setInterrupted( true );
    }
    /**
     * Die Methode wird aus einem Zusatzthread aufgerufen. Sie startet
     * den Algorithmus, und sorgt nach seiner Beendigung dafür, dass das
     * Fenster weiterhin schön aussieht
     */
    public void run(){
        // Zurzeit ist der Algorithmus nicht unterbrochen
        algorithmus.setInterrupted( false );
        // Den Algorithmus starten. Die Methode kehrt erst zurück, wenn der
        // Algo abgearbeitet oder unterbrochen wurde.
        algorithmus.run( progress );
        if( algorithmus.isInterrupted() ){
            /* 
             * Sollte der Algorithmus abgebrochen worden sein, wird die 
             * Progressbar zurück auf den Startwert gesetzt.
             * 
             * Die Methode "invokeLater" garantiert, dass dass der Code des 
             * übergebenen Runnable's im Dispatch-Thread
             * ausgeführt wird. Dies ist notwendig, weil Swing nicht
             * threadsicher ist.
             */
            SwingUtilities.invokeLater( new Runnable(){
                public void run(){
                    progress.setMinimum( 0 );
                    progress.setMaximum( 1 );
                    progress.setValue( 0 );
                    buttonStart.setEnabled( true );
                }
            });
        }
        else{
            /*
             * Der Algorithmus wurde korrekt beendet, ein Dialog soll den
             * Benutzer unterrichten, dass das Programm nun geschlossen wird.
             * 
             * Die Methode "invokeAndWait" garantiert, dass der Code des
             * Runnable-Objektes, welches übergeben wird, im
             * EventDispatcher-Thread ausgeführt wird. Ausserdem kehrt die Methode
             * erst dann zurück, wenn eben dieser Code ausgeführt wurde.
             */
            try {
                SwingUtilities.invokeAndWait( new Runnable(){
                    public void run(){
                        JOptionPane.showMessageDialog( Main.this, 
                                "Algorithmus erfolgreich beendet.", "Beendet",
                                JOptionPane.INFORMATION_MESSAGE );
                    }
                });
            } catch (InterruptedException e) {
                // Kann ja sein, das hier was schiefgeht...
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            // Programm beenden, nachdem der Dialog geschlossen wurde
            System.exit(0);
        }
    }
}
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
/**
 * Der Algorithmus stellt einige langwierige Berechnungen an, für deren
 * Erklärung hier leider keinen Platz gefunden werden kann.
 */
class Algorithmus{
    /**
     * Dieser Fortschrittsbalken wird benutzt, um den Fortschritt des Algos
     * anzuzeigen
     */
    private JProgressBar status;
    /**
     * Dieser Boolean wird auf "true" gesetzt, sollte der Algorithmus
     * abgebrochen werden.
     */
    private boolean interrupted = false;
    public Algorithmus(){
    }
    /**
     * Gibt an, ob der Algorithmus abgebrochen wird, oder wurde.
     * @return true, falls der Algorithmus stoppen soll
     */
    public boolean isInterrupted() {
        return interrupted;
    }
    /**
     * Gibt an, ob der Algorithmus abgebrochen werden soll.
     * @param interrupt true, falls der Algorithmus abgebrochen werden soll,
     *            andernfalls false.
     */
    public void setInterrupted( boolean interrupt ) {
        interrupted = interrupt;
    }
    /**
     * Die Hauptmethode des Algorithmuses, hier geschehen alle wichtigen
     * Berechnungen. Leider konnte die Weltformel hier nicht Platz finden,
     * deshalb beschränkt sich die Methode auf stupides hochzählen eines
     * Integers :-)
     * @param progress Der Fortschrittsbalken, welcher den Fortschritt des Algos
     *            anzeigen wird.
     */
    public void run( JProgressBar progress ) {
        // Den Fortschrittsbalken speichern wir als Instanzvariable. Das
        // hat den Vorteil, dass jede der 1000 Methoden des Algos Zugriff
        // auf die Progressbar hat.
        status = progress;
        int begin = 13;
        int end = 65;
        int current = begin;
        // Minimum und Maximum des Fortschrittbalkens setzen
        setMinMax( begin, end );
        while( current <= end && !isInterrupted() ){
            try{
                Thread.sleep( 100 ); // Kurz warten
            }
            catch( InterruptedException e ){
            }
            setValue( current++ ); // Hey! Es ist was passiert!
        }
        // Instanzvariable auf null setzen, damit der Garbagecollector
        // allenfalls Müll entfernen kann
        status = null;
    }
    /**
     * Setzt die minimalen und die maximalen Werte des Fortschrittbalkens. Die
     * Methode kehrt erst zurück, wenn die Werte tatsächlich gesetzt wurden.
     * @param min Das Minimum
     * @param max Das Maximum
     */
    private void setMinMax( final int min, final int max ) {
        try{
            /*
             * Diese Methode wird nicht aus dem Dispatcher-Thread aufgerufen. Da
             * Swing nicht threadsicher ist, müssen wir für eine Synchronisation
             * sorgen, deshalb wird hier "invokeAndWait" verwendet.
             */
            SwingUtilities.invokeAndWait( new Runnable(){
                public void run() {
                    status.setMinimum( min );
                    status.setValue( min );
                    status.setMaximum( max );
                }
            });
        }
        catch( Exception e ){
        }
    }
    private void setValue( final int value ) {
        /*
         * Der Algorithmus sollte nicht unnötig angehalten werden, nur damit die
         * Progressbar schön aussieht, deshalb "invokeLater". Es kann nun aber
         * passieren, dass der Algorithmus abbricht, nachdem er "setValue"
         * aufgerufen hat (und damit die Variable "status" auf null setzt), das
         * übergebene Runnable aber erst später ausgeführt wird. Aus diesem
         * Grund wird ein try-catch-Block benutzt, der die mögliche
         * NullPointerException abfängt. Alternativ könnte man mit dem Keyword
         * "synchronized" einen Mechanismus bauen, der verhindert dass die
         * Variable "status" verändert wird, während wir uns in dieser Methode
         * befinden (und dann eine einfache if-Abfrage machen).
         */
        SwingUtilities.invokeLater( new Runnable(){
            public void run() {
                try{
                    status.setValue( value );
                }
                catch( NullPointerException ex ){
                    // silent exception
                }
            }
        });
    }
}

Hier wird ein Frame verwendet. Anstelle eines Frames könnte man auch einen modalen Dialog benutzen, dieser würde solange er sichtbar ist, die Methode blockieren, welche dialog.setVisible(true) aufgerufen hat.

--Beni (01.09.2004, 01:56) --Bleiglanz (01.09.2004)