Threads, Timers, and responsive GUI's

by Chris Adamson

A little think-about-it for you:



What typically takes longer in java?


  1. Sorting 1,000 Long objects
  2. Reading one byte from Yahoo



You might figure that the sort is going to be worse, seeing as how it'll have to loop (or recur) through all those objects, and there are so many, and Java's an interpreted language after all, so that means it's sure to be slow. And reading a byte from a URL's input stream that's like, what, two lines maybe? Surely it'll be fast.



Well, here's the code:



import java.util.Random;
import java.net.*;
import java.io.*;
import java.util.Arrays;

public class WhosSlower {

public static URL theURL;
static {
try {
theURL = new URL ("http://www.yahoo.com/");
} catch (MalformedURLException murle) {
murle.printStackTrace();
}
}

public static void main (String[] arrrImAPirate) {
for (int i=1; i<=5; i++) {
System.out.println ("--- Trial #" + i +
" ---");
System.out.println ("Sort 1000 Longs: " +
sort1000Longs() + " ms");
System.out.println (
"Read 1 byte from URL: " +
getFirstByteFromURL() +
" ms");
}
}

/** sorts 1000 longs, returns the time it took
(doesn't count time to set up the array
in the first place)
*/
public static long sort1000Longs () {
Random rand = new Random();
Long[] longs = new Long[1000];
// populate array
for (int i=0; i < longs.length; i++) {
longs[i] = new Long (rand.nextLong());
}
// start clock, do the sort
long inTime = System.currentTimeMillis();
Arrays.sort (longs);
return System.currentTimeMillis() - inTime;
}

/** opens connection to theUrl, reads one byte,
returns the time it took
*/
public static long getFirstByteFromURL () {
long inTime = System.currentTimeMillis();
InputStream in = null;
try {
in = theURL.openStream();
in.read();
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
long outTime = System.currentTimeMillis();
// close up stream (not counting this
// in the race)
try {
in.close();
} catch (IOException ioe2) {
ioe2.printStackTrace();
}
return outTime - inTime;
}
}
}


And the results on a 300 MHz iBook running Mac OS X 10.2.4:




--- Trial #1 ---
Sort 1000 Longs: 30 ms
Read 1 byte from URL: 355 ms
--- Trial #2 ---
Sort 1000 Longs: 9 ms
Read 1 byte from URL: 123 ms
--- Trial #3 ---
Sort 1000 Longs: 4 ms
Read 1 byte from URL: 123 ms
--- Trial #4 ---
Sort 1000 Longs: 4 ms
Read 1 byte from URL: 129 ms
--- Trial #5 ---
Sort 1000 Longs: 4 ms
Read 1 byte from URL: 200 ms


And on a Windows box of unknown speed running Windows 2000:




--- Trial #1 ---
Sort 1000 Longs: 10 ms
Read 1 byte from URL: 361 ms
--- Trial #2 ---
Sort 1000 Longs: 0 ms
Read 1 byte from URL: 200 ms
--- Trial #3 ---
Sort 1000 Longs: 0 ms
Read 1 byte from URL: 150 ms
--- Trial #4 ---
Sort 1000 Longs: 0 ms
Read 1 byte from URL: 170 ms
--- Trial #5 ---
Sort 1000 Longs: 0 ms
Read 1 byte from URL: 171 ms


Setting aside the issue of Windows' low resolution for System.currentTimeMillis(), it's consistent that the sort is two orders of magnitude faster than the web read.



My point? Just that I think doing stuff in memory with java is faster than most developers generally think, and doing stuff on the network is slower than most developers think. I've met people who insist on returning Vectors or ArrayLists from methods because of "performance concerns" with converting those to fixed-length, strongly-typed arrays, yet will happily throw an RMI call into a for-next loop and iterate over it 20 times



The important but invisible difference between local and remote method calls sort of touches on an idea brought up in W. Keith Edwards' Core Jini, the idea that making the network transparent to the developer is, in fact, a bad idea. Of CORBA, RPC, and the like, he writes:


The hardest parts of building reliable distributed systems have to do with precisely those those things about the network that cannot be ignored by the programmer - the fact that the time required to access a remote resource may be orders of magnitude longer than accessing the same resource locally; the fact that networks fail in ways stand-alone systems do not; and the fact that networked systems are susceptible to partial failures of computations that can leave the system in an inconsistent state.

(first edition, p. 41)



So not only are our network calls slow, they're hazardous. There's no way to know that when our client calls

    happyServer.doStuff()

that the implementation of doStuff() on happyServer isnt something like:

    while (true) {}



In other words, we really don't have the right to expect that an RMI call will ever return.



And to make life more fun, let's assume that we're writing a GUI, and that this call is made from the AWT event dispatch thread, say, in response to a button click. As long as we block, possibly forever, we won't service the GUI and in fact, won't even get repainted if another window is dragged over ours.



Well.... that would suck, wouldn't it?



And considering how many Java GUI's are written for enterprise applications - typically distributed applications that use JDBC, RMI, CORBA, JMS, Jini, etc. - this seems like a problem that's going to come up a lot.



The solution, it seems, is in using threads, and letting them run as long as they need (possibly forever), updating the GUI when the threaded network call finishes. The network call can be isolated in its own thread, and when done, it can use a Swing "worker" - a Runnable called by the invokeLater or invokeAndWait in the SwingUtilities class - to update the GUI is a Swing-friendly way.



Well, that's well and good I suppose, but what does your app do in the meantime? If a user clicks a button and you immediately return (because you launched the thread), what do you do with the GUI in the meantime? Worse, what if the user clicks the button again - are you going to launch a second thread?



Now the problem is that the GUI has to know about the thread and what it's doing.



I discovered this problem when I was doing a project last year, one in which I decided that everything was going to have a very rigid model-delegate design: I'd pass around model objects and ask a factory for an appropriate "view", ie, some kind of custom JPanel I'd written to render that model object. I figured I needed to handle models in two states:


  1. the model is null, so we clear disable all the widgets in the panel
  2. the model is non-null, and has all the data, so we're ready to go.



This was nice and all, but sometimes I had models that would take 30 seconds to populate from a database call, during which I'd have to just spin the wristwatch, hourglass, or rainbow stress-pizza. I was trapped by my panels' need for a fully-populated model, and the trap with my design was that it was a false dichotomy, that there was a third state I hadn't dealt with. The "and" in the second item is a giveaway: I needed my panels to handle a new case where:


  • the model is non-null, but it doesn't have all the data yet, so we're not ready to go.



If we're willing to tolerate this state, then we can have a happy GUI again. I extended the tool interface with a thread-aware subinterface that also added a getStatus() String that the GUI could use to provide feedback, and a simple listener scheme that would fire off an event when the threaded operation was done (ie, when the model had its data and was usable). Some implementations of this subinterface also got a getPercentDone() method, which allowed me to provide a progress bar. At any rate, the panel's setModel() got trickier, but could now handle the various states:


  1. if model object is null, clear and disable widgets
  2. if model is non-null and not a running thread, call a poulateFromModel() to enable and fill in the widgets
  3. if model is non-null and is a running thread, disable the widgets, set up set up a Swing Timer (which runs every 500 ms, calls getStatus() on the model and resets a temporary label in the panel with the returned status), and also set up a listener (which when when called back with the thread-done message calls populateFromModel and stops the Timer)



You could also do this without the listeners, by requiring that the model objects be Threads, and then just having the panel check to see if the thread isAlive(), and using the Timer to poll it, either updating the status label if the thread's alive or populating the widgets and stopping the Timer if it's not. Just a question of design.



This approach gives my app more responsiveness in the places I've implemented it, since something's always happening - for example, a count of loaded database records updates every 1/2 sec until they're all available, at which point I can put them in a JList or something.



The two cases it doesn't help much with are long RMI calls, and the theoretical case where the call never returns. A single RMI call that takes 30 seconds for the server to process doesn't give me a status update half-way through - it's just a single line that my thread blocks on. True, I'm not blocking the GUI anymore, but all I can do for the user is to put up a "Waiting" type message or an animated GIF. As for the call that never returns, my user could go elsewhere in the GUI, but we're now leaking Threads, memory, and network resources, so a day of reckoning may eventually come, perhaps in the form of an OutOfMemoryError. If my user exits the panel, I can't really assassinate the thread with Thread.stop(), because it's deprecated and deadlock-prone (who knows what state it will leave the RMI call in?)



So, problem partially solved - the user experience is vastly better than when it would block the GUI for a 10-30 second database call, but there are still cases I'd like to be able to handle and can't.



How do we write more responsive network GUI's? Does Java give us the right tools? Should it force us to use better practices? Let's have some ideas...


3 Comments

jasontm
2003-03-27 13:49:46
why not..
just notify the user after a set period of time that the operation is taking unusually long and give an option to abort the thread or continue waiting. just a thought.
jasontm
2003-03-27 13:51:47
sorry, server error
i kept getting internal server errors when trying to post my comment, so i didn't see it get posted. :P


sorry about the multiplication..

sarahkim
2003-03-28 13:48:25
server error - fixed
The server error has been fixed and I've cleaned up the multiple posts.


sarah
O'Reilly Network