Google wants you to love its Android AsyncTask pattern. I admit that, coming from an embedded C/C++ background, I was a little skeptical about handing over my long-running I/O to a black-boxed thread pool. In time, though, I appreciated the class and its simplicity. It worked well, and I didn’t have to fool with it.

This past week however, I ran into some unexpected behavior. A user reported very slow I/O on one of my applications after he upgraded from an older device to a newer one. This was the opposite of what one would expect, so I quickly threw together a rudimentary test to pull some metrics.

Consider the following code snippet:

for (int i=0; i<5; i++) {
     AsyncTask1 task = new AsyncTask1();
     task.execute();
}

Even without knowing what is going on inside the AysncTask1 implementation, in my mind, the intention is to launch five of these tasks. So for the purpose of my test, I simulated some long-running I/O by overriding the doInBackground function of AsyncTask1 to sleep for 1,000 milliseconds.

@Override
protected Object doInBackground(Object... arg0)
try {
         Thread.sleep(ONE_SECOND);
     } catch (InterruptedException e) {
// TODO Auto-generated catch block
          e.printStackTrace();
     }
     return null;
}

After putting in some basic debug timers, I executed the code on two AVD instances: one running Eclair (Android 2.0) and one running Jelly Bean (Android 4.2). The results surprised me. Have a look for yourself (Figure A).
Figure A

That’s right — Android 2.0 left the latest and greatest version of the OS eating dust. You don’t have to be all that astute to multiply the one-second delay we hardcoded into our AsyncTask by the five iterations of our for-loop to guess what is happening: Android is running the tasks serially instead of in parallel. I started poking around on the forums and eventually was directed back to Google’s AsyncTask documentation. About a third of the way down the page, you’ll find the following note:

“Starting with HONEYCOMB, tasks are executed on a single thread to avoid common application errors caused by parallel execution.

If you truly want parallel execution, you can invoke executeOnExecutor(java.util.concurrent.Executor, Object[]) withTHREAD_POOL_EXECUTOR.”

From my perspective this is a little counterintuitive. Plus, I’d like a behavior change of this magnitude to be announced by blaring trumpets and a cry of Hear ye! Hear ye! Nevertheless, preserving the behavior seen on Eclair is a simple fix.

int currentapiVersion = android.os.Build.VERSION.SDK_INT;
for (int i=0; i<5; i++) {
     AsyncTask task = new AsyncTask1();
     if (currentapiVersion >=
          android.os.Build.VERSION_CODES.HONEYCOMB) {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
     } else {
          task.execute();
     }
}

As you can see from the chart below, the execution speed was restored (Figure B).
Figure B

For my particular application, the impact wasn’t huge if the change in behavior has been around since the release of Honeycomb and I’m just now hearing about it. Still, like most developers, I want to provide users with an optimal experience. I assumed (incorrectly) that a newer OS meant things would run at least at the same speed, if not faster. Lesson learned.

If you haven’t looked at the AsyncTask documents for a while, I recommend reacquainting yourself with them. In my code, it is unlikely that I will execute asynchronous tasks in the future without using the onExecutor method.