Foxtrot

SourceForge.net Logo Java.net Logo

Overview

The Problem

Asynchronous Solution

Synchronous Solutions

Foxtrot

Foxtrot & Swing

Foxtrot's synchronous solution: Worker

The Foxtrot framework is based on a different approach than asynchronous solutions. While a worker thread is still used to execute time-consuming tasks, SwingUtilities.invokeLater() is not used.
The main problem of the asynchronous solution is that it lets the listener continue during the execution of the task; in the most common cases, the listener returns immediately. This is done to allow the Event Dispatch Thread to dequeue the next event and process it, so that the GUI does not appear to be frozen.
In contrast, Foxtrot lets the Event Dispatch Thread enter but not return from the listener method; instead, it re-routes the Event Dispatch Thread to continue dequeuing events from the Event Queue and processing them. Once the worker thread has finished, the Event Dispatch Thread is re-routed again, continuing the execution of the listener method (in the most common cases, just returning from the listener method).

This approach is similar to the one used to display modal dialogs in AWT or Swing; unfortunately all classes that allow dialogs to re-route the Event Dispatch Thread inside a listener to continue dequeueing and processing events are private to package java.awt. However, AWT and Swing architects left enough room to achieve exactly the same behavior, just with a little more coding necessary in the Foxtrot implementation.

The main idea behind the synchronous solution is to prevent the Event Dispatch Thread from returning from the time-consuming listener, while having the worker thread executing the time consuming code, and the Event Dispatch Thread continuing dequeuing and processing events from the Event Queue. As soon as the worker thread is done with the task execution, the Event Dispatch Thread will resume its execution of the listener method, and eventually return. That's why these solution are synchronous: the code in the event listener and the code of the task are executed sequentially, as the appear in the code.

Take a look at the code below that uses the Foxtrot API.

Let's concentrate on the button's listener (the actionPerformed() method): the first statement, as in the freeze example, changes the text of the button and thus posts a repaint event to the queue.
The next statement uses the Foxtrot API to create a Task and post it to the worker queue, using the foxtrot.Worker class. The Worker.post() method is blocking and must be called from the Event Dispatch Thread.
When initialized, the Worker class starts a single worker thread to execute time-consuming tasks, and has a single worker queue where time-consuming tasks are queued before being executed.
When a Task is posted, the worker thread executes the code contained in Task.run() and the Event Dispatch Thread is told to contemporarly dequeue events from the Event Queue. On the Event Queue it finds the repaint event posted by the first setText() invocation, and processes it.
The Worker.post() method does not return until the time-consuming task is finished or throws an exception. When the time-consuming task is finished, the Worker class tells the Event Dispatch Thread to stop dequeueing events from the Event Queue, and to return from the Worker.post() method. When the Worker.post() method returns, the second setText() is called and the listener returns, allowing the Event Dispatch Thread to do its job in the normal way.
This is why we call this solution synchronous: the event listener does not return while the code in the time-consuming task is run by the worker thread.

Let's compare this solution with the asynchronous ones, to see how it resolves their drawbacks:

  • Simple exception handling: exceptions can be caught and rethrown within the listener. No need for chained if-else statements. The only drawback is that the listener is required to always catch Exception from the Worker.post() method when posting Tasks (this is not necessary when using Jobs).
  • Note the symmetry: the two setText() calls are both inside the listener.
  • No callback methods, whether the Task completed successfully or threw an exception.
  • The code after the time-consuming task is independent of the time-consuming task itself. This allows refactoring of Worker.post() calls, and it is possible to execute different code after Worker.post() depending on the place from where we want to execute the time-consuming task.
  • Code written after Worker.post() is always executed after the code in Task.run(). This greatly improve code readability and semplicity. No worries about code executed after Worker.post().
  • No nesting of Worker.post() is necessary, just 2 consecutive Worker.post() calls.

Legend
Main Thread
Event Dispatch Thread
Foxtrot Worker Thread

public class FoxtrotExample extends JFrame
{
   public static void main(String[] args)
   {
      FoxtrotExample example = new FoxtrotExample();
      example.setVisible(true);
   }

   public FoxtrotExample()
   {
      super("Foxtrot Example");

      final JButton button = new JButton("Take a nap !");
      button.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent e)
         {
            button.setText("Sleeping...");

            String text = null;
            try
            {
               text = (String)Worker.post(new Task()
               {
                  public Object run() throws Exception
                  {
                     Thread.sleep(10000);
                     return "Slept !";
                  }
               });
            }
            catch (Exception x) ...

            button.setText(text);

            somethingElse();
         }
      });

      setDefaultCloseOperation(DISPOSE_ON_CLOSE);

      Container c = getContentPane();
      c.setLayout(new GridBagLayout());
      c.add(button);

      setSize(300,200);

      Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
      Dimension size = getSize();
      int x = (screen.width - size.width) >> 1;
      int y = (screen.height - size.height) >> 1;
      setLocation(x, y);
   }
}