Using the Actor pattern in GUIs

GUI interfaces often start long running tasks, for example, downloading a file from a remote machine. To ensure the GUI refreshes, it's common to start the task in a separate thread. This thread generally needs to update the GUI, for example, to show a progress bar. However, GUI toolkits tend not to be thread-safe, making such updates problematic. The actor pattern (also known as the active object pattern), can help solve these problems.

An actor is an object with its own thread. Method invocations on the object return immediately, with the body of the method queued to be executed by the object's thread. If all objects that need to update the GUI are made into actors, with a single thread shared between them all, the GUI is guaranteed to be updated by a single thread.

Consider an application that brews a cup of tea. Brewing a cup of tea is a long process, taking between 3 and 5 minutes. Obviously we don't want to freeze the GUI during the brewing, so we'll need to brew in a separate thread, and notify the GUI when we're finished. If you're not British, and can't relate to the example of brewing a proper cup of tea, mentally replace it with long running database query. But tea's more interesting. Our tea maker will look something like this:

public interface TeaMakerListener {

    public void handleBrewingComplete();

}

public interface TeaMaker {

        public void setListener(TeaListener listener);
        public void brew();

}
        

brew() will be invoked in a separate thread. When it completes, we'll display a message in the GUI. This message needs to be displayed by the thread responsible for the GUI. In Swing, this can be done through the invokeLater() method. The GUI code will look something like:

public class TeaMakerView implements TeaMakerListener {

    TeaMakerView(TeaMaker teaMaker) {
        teaMaker.setListener(this);
        ...
    }

    ...

    public void handleBrewingComplete() {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                label.setText("Tea's ready!");
            }
        });
    }

}
        
handleBrewingComplete() behaves like a method on an actor - when the implementation of TeaMaker invokes it, it will return immediately, queueing the setLabel() invocation for execution in the Swing thread.

The new Runnable(), invokeLater() dance becomes tedious, not to mention error prone, very quickly. Luckily, we can employ some poor-man's aspect oriented programming, and use a dynamic proxy to ensure that all invocations on TeaMakerView are executed in the Swing thread:

public class TeaMakerView implements TeaMakerListener {

    TeaMakerView(TeaMaker teaMaker) {
        teaMaker.setListener(makeActor(this));
        ...
    }

    private TeaMakerListener makeActor(TeaMakerListener listener) {
        InvocationHandler handler = new SwingInvocationHandler(listener);
        return (TeaMakerListener)Proxy.newProxyInstance(TeaMakerListener.class.getClassLoader(),
                                                        new Class[] {TeaMakerListener.class},
                                                        handler);
    }

    public void handleBrewingComplete() {
        label.setText("Tea's ready!");
    }

}

class SwingInvocationHandler implements InvocationHandler {
	
    private Object delegate;

    SwingInvocationHandler(Object delegate) {
        this.delegate = delegate;
    }

    public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {		
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    method.invoke(delegate, args);
                } 
                catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });		
        return null;
    }
	
}
        
Factoring out makeActor() is left as an exercise for the reader.

I originally used this pattern as part of a project written in Python, using the wonderful PyGTK. I rewrote the code here in Java for shameless mass appeal. A Python version of the example's available, though.