21

This was asked in one of the Android interviews. I was asked whether it's possible to start another async task (let it be Task2) from doInBackground() method of async task 1(let it be Task1). I had gone through the docs which say the following:

The task instance must be created on the UI thread.

execute(Params...) must be invoked on the UI thread.

As per these statements, I think that it shouldn't be possible to start a task from background method of another task. Also, async task has UI methods (which cannot be used on a background thread), so that strengthened my argument and I answered it as not possible.

On checking on a simple demo app, I saw that it's indeed possible to do so. Some demo code:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
        init();
        Log.v ("gaurav", "Thread is : " + Thread.currentThread().getName());
        Task1 task = new Task1();
        task.execute();
    }

class Task1 extends AsyncTask {
    @Override
    protected Object doInBackground(Object... params) {
        // TODO Auto-generated method stub
        Log.v ("gaurav", "Thread task 1 is : " + Thread.currentThread().getName());

        Task2 task = new Task2();

        task.execute();
        return null;
    }
}

class Task2 extends AsyncTask {
    @Override
    protected Object doInBackground(Object... params) {
        // TODO Auto-generated method stub
        Log.v ("gaurav", "Thread task 2 is : " + Thread.currentThread().getName());

        Log.v ("gaurav", "Task 2 started");
        return null;
    }
}

I get following logs indicating successful execution :

> 08-07 09:46:25.564: V/gaurav(2100): Thread is : main 08-07
> 09:46:25.564: V/gaurav(2100): Thread task 1 is : AsyncTask #3 08-07
> 09:46:25.564: V/gaurav(2100): Thread task 2 is : AsyncTask #4 08-07
> 09:46:25.564: V/gaurav(2100): Task 2 started

I have checked this on ICS, KK and L device and it works fine for all.

One reason I could think of is that I'm not overriding any UI methods and doing any UI updation in my second task, hence it doesn't cause any problems, but I'm not sure. Even if that's the case, it violates the threading rules mentioned in the developer guide.

As a reference, I checked out this link too : Start AsyncTask from another AsyncTask doInBackground() but the answer states to start the second task using runOnUiThread() method inside doInBackground(). I'd like some help on what's going on here. Thanks.

Community
  • 1
  • 1
gaurav jain
  • 3,119
  • 3
  • 31
  • 48
  • Another reason I have used similar questions in interviews is to determine if the candidate knows about breaking thread-behaviour changes in AsyncTasks from Honeycomb upwards: http://stackoverflow.com/questions/21165505/mutliple-asynctasks-with-sequential-execution – adelphus Aug 07 '15 at 10:13
  • @adelphus, I couldn't quite understand the meaning of "Starting with HONEYCOMB, tasks are executed on a single thread to avoid common application errors caused by parallel execution." It does define about serial execution, but the nested tasks would still have their onPreExecute() / onPostExecute() run on background thread in context of my question? Or as the answer below says about class load on app start, in which case all nested tasks' onPre/onPost methods run on UI thread regardless from where the task was started? – gaurav jain Aug 08 '15 at 07:26
  • There are two issues here: my comment is talking about the fact that multiple AsyncTasks used to run in parallel with 1 thread per task. Since Honeycomb, multiple AsyncTasks are now queued so they all run on a single thread - only 1 can run at a time. The change broke a lot of code because devs assumed tasks ran in parallel. Your question about whether onPreExecute/onPostExecute executes on the UI thread is a different issue, but yaa110 describes it well in his answer. – adelphus Aug 08 '15 at 11:50

2 Answers2

15

Let's change your code to the following:

class Task1 extends AsyncTask {
    @Override
    protected Object doInBackground(Object... params) {
        // TODO Auto-generated method stub
        Log.v ("gaurav", "Thread task 1 is : " + Thread.currentThread().getName());

        Task2 task = new Task2();
        task.execute();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.v ("gaurav", "Log after sleeping");

        return null;
    }
}

class Task2 extends AsyncTask {
    @Override
    protected Object doInBackground(Object... params) {
        // TODO Auto-generated method stub
        Log.v ("gaurav", "Thread task 2 is : " + Thread.currentThread().getName());

        Log.v ("gaurav", "Task 2 Started");
        return null;
    }
}

Now the LogCat returns:

08-07 06:13:44.208    3073-3073/testapplication V/gaurav﹕ Thread is : main
08-07 06:13:44.209    3073-3091/testapplication V/gaurav﹕ Thread task 1 is : AsyncTask #1
08-07 06:13:49.211    3073-3091/testapplication V/gaurav﹕ Log after sleeping
08-07 06:13:49.213    3073-3095/testapplication V/gaurav﹕ Thread task 2 is : AsyncTask #2
08-07 06:13:49.213    3073-3095/testapplication V/gaurav﹕ Task 2 Started

As you can see the Task 2 is executed after the end of the Task 1 execution (even after sleeping for 5 seconds). It means the second task would not be started until the first one is done.

Why? The reason is behind the source code of AsyncTask. Please consider the execute() method:

public synchronized void execute(final Runnable r) {
    mTasks.offer(new Runnable() {
        public void run() {
            try {
                r.run();
            } finally {
                scheduleNext();
            }
        }
    });
    if (mActive == null) {
        scheduleNext();
    }
}

and scheduleNext() method:

protected synchronized void scheduleNext() {
    if ((mActive = mTasks.poll()) != null) {
        THREAD_POOL_EXECUTOR.execute(mActive);
    }
}

The most important keyword in these methods is synchronized which ensures these methods would be run only in one thread at the same time. When you call the execute method, it offers a new Runnable to mTask which is an instance of ArrayDeque<Runnable> class which works as a serializer of the different requests at different threads [more info]. If there was no executed Runnable (i.e. if (mActive == null)), the scheduleNext() would be called, otherwise, the scheduleNext() in the finally block would be called after the (for any reason) end of current executed Runnable. All Runnables are executed on a separate thread by THREAD_POOL_EXECUTOR.

What's wrong with the execution of AsyncTask from other threads? Starting with Jelly Bean, an AsyncTask is class-loaded at application start on the UI thread, so that the callbacks are guaranteed to occur on the UI thread, however, prior to the Jelly Bean release, if another thread creates the AsyncTask the callbacks may not occur on the correct thread.

So, AsyncTask implementations should be called from the UI thread only on platforms prior to Jelly Bean (+ and +).


Clarification: please consider the following example which simply clarifies the differences between different platform releases of Android:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main2);

    new Thread() {
        @Override
        public void run() {
            Task1 task = new Task1();
            task.execute();
        }
    }.start();
}

class Task1 extends AsyncTask {
    @Override
    protected Object doInBackground(Object... params) {
        return null;
    }
}

It works fine on Android 5.1, but crashes with the following exception on Android 2.3:

08-07 12:05:20.736      584-591/github.yaa110.testapplication E/AndroidRuntime﹕ FATAL EXCEPTION: Thread-8
    java.lang.ExceptionInInitializerError
            at github.yaa110.testapplication.Main2Activity$1.run(Main2Activity.java:21)
     Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
            at android.os.Handler.<init>(Handler.java:121)
            at android.os.AsyncTask$InternalHandler.<init>(AsyncTask.java:421)
            at android.os.AsyncTask$InternalHandler.<init>(AsyncTask.java:421)
            at android.os.AsyncTask.<clinit>(AsyncTask.java:152)
            at github.yaa110.testapplication.Main2Activity$1.run(Main2Activity.java:21)
Community
  • 1
  • 1
  • 1
    As per your comment, I infer that it is indeed possible. The only point is that the tasks would be queued as by default serial executor is used to run the tasks. If that's the case shouldn't the doc specify that this is possible. The 'must' lets me assume that it'll cause a crash. – gaurav jain Aug 07 '15 at 10:30
  • The above part is pretty clear to me, which summarizes that Android would queue up the requests using mTask which is a serializer. But the last part where you mentioned that this should be done only on API >= JellyBean is little tricky because I tested the same code on ICS. Can you check why this works on ICS ? Or is it kind of a 'might work' scenario which may not run successfully every time? – gaurav jain Aug 07 '15 at 11:22
  • @gauravjain Unfortunately, I don't have access to ICS (let me get the Genymotion emulator of ICS). But if you want to check if it works fine on ICS, please try to change the `text` of a `TextView` inside the `onPostExecute()` method of `Task 2`. I think, it is not possible to change the UI prior to Jelly Bean in this scenario. –  Aug 07 '15 at 11:28
  • I checked it and updated a button text in onPreExecute() and onPostExecute() of task 2. Tested it on ICS and KK, works fine without any error, which makes me understand that though task 2 has been started from a background thread, it's onPreExecute() / onPostExecute() runs on UI thread regardless of the call (this also occurs on ICS emulator/device). – gaurav jain Aug 08 '15 at 07:23
  • If I print the logs inside onPreExecute() / onPostExecute() methods of nested task (task 2), it shows these methods being run on a background thread. Still UI updation is possible from these methods. Sorry, but I'm a bit confused, I'm verifying this on API 10 (Android 2.3.7) emulator now. – gaurav jain Aug 08 '15 at 07:26
  • 1
    @gauravjain the test for checking if UI updates are running on the UI thread is *not guaranteed* to be called - sometimes it will work, sometimes an exception will be thrown. It depends upon the current state of the UI. Try performing lots of UI updates to see if it really works. – adelphus Aug 08 '15 at 12:06
  • @adelphus, I observed a difference of behavior on GB for 2 different scenarios : Firstly, launching a task from doInBackground() of first task (works fine even on GB emulator). Secondly, launching a task from a thread as yaa110 mentioned in the *clarification* part, and it crashes straightway 100% times. What's the difference, in both cases, a task is being started from a background thread. Regarding your comment. I'll try updating more UI to see if it fails sometimes, because even if I update text of 5 buttons, it works fine 100% times on all.(from GB to L). – gaurav jain Aug 08 '15 at 14:11
  • Although I've accepted the answer, just want to add another point, that ideally this is not right to launch an async task from a background thread, because as @adelphus points out, it's possible to get an exception in some cases where you do a lot of UI updates in child task. – gaurav jain Aug 10 '15 at 05:33
  • AsyncTask is now deprecated – Paula Livingstone Aug 18 '20 at 11:21
-1
public class MainActivity extends Activity {

    private final static String TAG = "ThreadingAsyncTask";
    private ImageView mImageView;
    private ProgressBar mProgressBar;
    private int mDelay = 500;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mImageView = (ImageView) findViewById(R.id.imageView);;
        mProgressBar = (ProgressBar) findViewById(R.id.progressBar);

        final Button button = (Button) findViewById(R.id.loadButton);
        button.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                new LoadIconTask().execute(R.drawable.cheetah);
            }
        });
        final Button otherButton = (Button) findViewById(R.id.otherButton);
        otherButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "I'm Working",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }


    class LoadIconTask extends AsyncTask<Integer, Integer, Bitmap> {
        @Override
        protected void onPreExecute() {
            mProgressBar.setVisibility(ProgressBar.VISIBLE);
        }
        @Override
        protected Bitmap doInBackground(Integer... resId) {
            Bitmap tmp = BitmapFactory.decodeResource(getResources(), resId[0]);
            // simulating long-running operation
            for (int i = 1; i < 11; i++) {
                sleep();
                publishProgress(i * 10);
            }
            return tmp;
        }
        @Override
        protected void onProgressUpdate(Integer... values) {
            mProgressBar.setProgress(values[0]);
        }
        @Override
        protected void onPostExecute(Bitmap result) {
            mProgressBar.setVisibility(ProgressBar.INVISIBLE);
            mImageView.setImageBitmap(result);
        }
        private void sleep() {
            try {
                Thread.sleep(mDelay);
            } catch (InterruptedException e) {
                Log.e(TAG, e.toString());
            }
        }
    }
}
dukedevil294
  • 1,285
  • 17
  • 31
PeterPan
  • 1
  • 1