Foxtrot: synchronous solution
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 return immediately. This is done to allow the
Event Dispatch Thread to dequeue the next event and process it.
In contrast, Foxtrot lets the Event Dispatch Thread enter but not return from the listener method, instead rerouting 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 rerouted again, 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 reroute 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 finished, the Event
Dispatch Thread will return from the listener method. That's why
this solution is synchronous.
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 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() , and processes it.
The Worker.post() method does not return until the time-consuming task is finished.
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.
- Note the symmetry: the two
setText() are both inside the listener.
- No
get() method, whether you expect a result or not. If there are exceptions, they will be rethrown.
- 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 afterwards. 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.
We have used Foxtrot heavily in a large Swing application, and the code benefited greatly from the use of the
Foxtrot framework.
Acknowledgements
Marco Cravero had the first idea of exploring how dialogs work to find a better solution for using threads in Swing.
Simone Bordet implemented the Foxtrot API following this idea.
Luca Berra suggested the 'Foxtrot' name.
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(EXIT_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);
}
}
|
Legend |
Main Thread |
Event Dispatch Thread |
Foxtrot Worker Thread |
|
|