Threads and responsiveness

29 November 2010

Touch screen phones have given us an important new way of interacting with computers. A great user interface can give the feeling of getting things done effortlessly, with minimal interference between your intentions and your actions.

However, there's one thing that can destroy that feeling of effortlessness, even in an otherwise great UI: lack of responsiveness. If you tap or drag and experience a delay before seeing the result of your action, you'll have to start taking that delay into account in everything you do. The illusion of effortlessness vanishes.

Topics covered:

  • The main GUI thread, and what not to do with it
  • Responsiveness
  • Multi-threaded programming with Executors

How do applications become unresponsive?

Most phones have a single processor and yet still manage to give the illusion that they are handling a lot of different tasks simultaneously. In reality, the processor is sharing its time, doing a little bit of task A and then putting it to one side to work on task B. The illusion of concurrency is maintained for two reasons:

  1. It happens fast, and a single task can get many little slices of processing time per second, giving the illusion of constant progress.
  2. Some tasks consist of bursts of activity followed by long waits for network responses, user input etc. During that time, the processor would be doing nothing anyway, so it's free to work on its other responsibilities.

What's true at the system level can also be true inside your app. If you have two separate things to accomplish, you can create two separate "threads of execution" and create the illusion that they are happening simultaneously. (If the processor had multiple cores, they could be literally simultaneous, but we'd still have limits - what if both cores were waiting for network responses? We'd be back in the same boat.)

Failing to do this properly is what can lead to unresponsive applications. Doing too much with one thread, especially with the main thread, can have terrible consequences for user experience.

Let's make this real with a quick app.

An Unresponsive application

Have a look at this highly deficient Activity. This app is specially designed to annoy the hell out of any user who tries to interact with it.

package info.ragtag.unresponsive; // imports omitted. /** * Demonstration of how annoying it can be when a slow-running * method blocks the main thread. */ public class UnresponsiveActivity extends Activity implements OnClickListener { private Handler mainThreadHandler; private volatile int clicks = 0; /** * Runnable object who's only purpose is to * block the thread it's running on with long * sleeps. */ private Runnable constantSleeper = new Runnable() { public void run() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // on waking, post another sleep command to the // handler to run after 50 milliseconds. Note that // 'this' refers to the immediate anonymous class, // not the containing class mainThreadHandler.postDelayed(this, 50); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // hook up the button Button button = (Button) findViewById(R.id.main_button); button.setOnClickListener(this); // set up a Handler to allow us to interact with the // current thread. Since activities are created by the // main thread, this handler will latch onto that. mainThreadHandler = new Handler(); mainThreadHandler.post(constantSleeper); } /** * Listen for button clicks and set text on the TextView. * Note that this action has to be performed by the main * thread because it involves GUI actions. */ public void onClick(View button) { TextView textView = (TextView) findViewById(R.id.text_box); textView.setText(++clicks + " clicks counted."); } }

If you fire it up, you'll find that any time you click on the button, it will be up to two seconds before you will see the result of your click. You will not even see the visual feedback that indicates a click has been registered until the main thread is freed up (by returning from the sleep() method).

The reason is simple: we failed to respect the main application thread.

You see, Android has to get a lot done with limited resources. The Handler object mainThreadHandler is an interface through which we can ask a thread to do things. Here we have one for the main thread, because Handlers automatically start a relationship with the thread in which they're instantiated. We abuse this privilege by posting a job called constantSleeper which 'blocks' the thread continually. Every time it wakes up, it leaves just 50ms for other processes to run before it demands to run again.

Events like clicks have to be processed by the main thread too. When a click occurs, the news that it has happened gets put on the main thread to be processed as fast as possible. But all the OnClickListeners that have signed up for click notifications won't hear a thing if there's a resource hog blocking up the thread. In this case, the notifications have to take place when constantSleeper is kind enough to let everyone else get some main thread love.

Hang on a second. Isn't this all a bit unrealistic?

OK, so you're not going to put the main thread to sleep in a real application. But if you replace Thread.sleep(2000) with an HTTP request which takes 2 seconds (or longer) to get a response, you will suffer exactly the same problem.

This kind of stuff should never, ever be done on the main thread. So where should we do it?

The Executor

We need to start another thread. Now we could do this manually, but that's not the Java way any more. Since version 5, Java has shipped with a great library for that sort of thing, java.util.concurrent. If you're new to it, the JavaDoc can be kind of overwhelming, but I assure you that with a bit of practice and familiarity, you will soon be up and running.

To show you how simply we can deal with our problem using this library, take a look at ResponsiveActivity below:

package info.ragtag.unresponsive; // imports omitted /** * Demonstration of how annoying it can be when a slow-running * method blocks the main thread. */ public class ResponsiveActivity extends Activity implements OnClickListener { private Executor secondaryThreadExecutor = Executors.newSingleThreadExecutor(); private volatile int clicks = 0; /** * Runnable object who's only purpose is to * block the thread it's running on with long * sleeps. */ private Runnable constantSleeper = new Runnable() { public void run() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } secondaryThreadExecutor.execute(this); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // hook up the button Button button = (Button) findViewById(R.id.main_button); button.setOnClickListener(this); // make the Executor deal with constantSleeper secondaryThreadExecutor.execute(constantSleeper); } /** * Listen for button clicks and set text on the TextView. * Note that this action has to be performed by the main * thread because it involves GUI actions. */ public void onClick(View button) { TextView textView = (TextView) findViewById(R.id.text_box); textView.setText(++clicks + " clicks counted."); } }

I guarantee you, this Activity won't lag, even though it's doing at least as much work as before. All of the sleeping takes place on the thread which is managed by secondaryThread, while the main thread is free to respond to clicks promptly.

End note

So why does main thread have to do so much by itself in the first place? The answer is that the nature of GUI's make them vulnerable to deadlock.

A good explanation of why this is the case can be found here, in an excerpt from Java Concurrency in Practice, perhaps the best discussion of Java concurrency you'll find today. I can't recommend this book highly enough.

Related tags: concurrency, handler, responsiveness, threads

Comments are closed.

Comments have been closed for this post.