Saturday, April 7, 2012

Inside AsyncTask

Most likely, AsynkTask is used by every Android developer. Being very simple and limited in earlier Android, AsyncTask became powerful instrument since 11 (first Honeycomb) sdk. But in some reason, this very useful class was deprived of due attention in official documentation, which doesn't cover even half of object's scalability and power. Let's skip all the obvious things, that can be read in variety of documents (including official documentation) and take a look inside AsyncTask.
First of all, every background operation (AsyncTask#doInBackground) is executed (submitted) by Executor, i.e. system reserves static thread pool to optimize process of previously created threads reusing (since new Thread creation is quite expensive operation). Second, in the evolution of Android SDK this Executor has undergone significant changes: been finally defined in early sdk:
 private static final ThreadPoolExecutor sExecutor = ... 
it became flexible for Honeycomb sdk:
private static volatile Executor sDefaultExecutor

Old implementation is not very interesting and quite boring, so let's take a look to the new one. It offers us 2 different implementation for Executor to cover different execution strategy: one is used to execute tasks in parallel, another - one task at time in serial order (of course, serialization is global to a particular process only). Despite the fact, that synchronous execution is default, during debug we can see, that parallel executor is used by default (most likely it's set by AsynkTask#setDefaultExecutor method).

Let's look closely to proposed implementations. First is parallel executor. This approach is inherited from old sdk and default for sdk 11 and above:
   /**  
    * An {@link Executor} that can be used to execute tasks in parallel.  
    */  
   public static final Executor THREAD_POOL_EXECUTOR  
       = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,  
           TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);  

   private static final int CORE_POOL_SIZE = 5;  
   private static final int MAXIMUM_POOL_SIZE = 128;  
   private static final int KEEP_ALIVE = 1;  

   private static final BlockingQueue<Runnable> sPoolWorkQueue =  
       new LinkedBlockingQueue<Runnable>(10);  

   private static final ThreadFactory sThreadFactory = new ThreadFactory() {  
     private final AtomicInteger mCount = new AtomicInteger(1);  
     public Thread newThread(Runnable r) {  
       return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());  
     }  
   };  

So, as we can see, pool is keeping 5 threads, whereas max number of available threads is 128. I think this is enough. Task are waiting for their turn to execution in BlockingQueue.

Synchronous executor is based on parallel:
   private static class SerialExecutor implements Executor {  
     final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();  
     Runnable mActive;  
     public synchronized void execute(final Runnable r) {  
       mTasks.offer(new Runnable() {  
         public void run() {  
           try {  
             r.run();  
           } finally {  
             scheduleNext();  
           }  
         }  
       });  
       if (mActive == null) {  
         scheduleNext();  
       }  
     }  
     protected synchronized void scheduleNext() {  
       if ((mActive = mTasks.poll()) != null) {  
         THREAD_POOL_EXECUTOR.execute(mActive);  
       }  
     }  
   }  
For this executor ArrayDeque is used as tasks holder. It's a good choice, since we don't need internal synchronization. Before actual task submission executor checks that we don't have active task now and schedule new only in this case. Consistency of tasks execution is provided by Runnable wrapper (this wrapper is created for every new task), which invokes scheduleNext at the end of it's execution. Direct submission is performed by THREAD_POOL_EXECUTOR.
Nothing forbids us to create own implementation for executor to meet our needs. How to use is will be considered below.

But first let's answer 2 questions: "how correct cancel of AsyncTask is ensured ?" and "why AsyncTask can be executed only once?". To answer these questions let's take a look constructor:
     private final WorkerRunnable<Params, Result> mWorker;  
     private final FutureTask<Result> mFuture;  
     
     public AsyncTask() {  
       mWorker = new WorkerRunnable<Params, Result>() {  
         public Result call() throws Exception {  
           ...  
         }  
       };  
       mFuture = new FutureTask<Result>(mWorker) {  
         @Override  
         protected void done() {  
           ...  
         }  
     }; 

     private static abstract class WorkerRunnable implements Callable {
         Params[] mParams;
     }
 

Callable mWorker is used to preserve input params and return result of computation (AsyncTask#doInBackground):
     
        mWorker = new WorkerRunnable() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);

                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                return postResult(doInBackground(mParams));
            }
        };

Last expression of call method executes our asynchronous operation and posts result via internal handler to ui thread (by calling AsyncTask#onPostExecute). mFuture being a FutureTask can be submitted to an Executor for asynchronous execution:
     
    public final AsyncTask execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    public final AsyncTask executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }

Now we can easily answer our questions:
1) AsyncTask cancellation is performed by FutureTask#cancel
2) initial check in executeOnExecutor doesn't allow for AsyncTask to be executed more than once. It's essential since once the computation of FutureTask has completed, the computation cannot be restarted (it laid the internal basis for this object).

And in the end I want to consider two very "tasty" methods (available from sdk 11). First of them is
 public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,  
       Params... params)   
It's called with default (parallel) executor in AsyncTask#execute. But it's interesting for us because it allows to execute tasks with certain executor and we can use not only proposed by sdk, but own implementation too:
     new AsyncTask<Void, Void, String>() {  
       ...  
     }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);  

Another very useful method in new api is
     public static void execute(Runnable runnable) {
        sDefaultExecutor.execute(runnable);
    }  

It allow us to execute our Runnables in global pool AsyncTask's global pool. This may freeing us of having own thread pools and save some system resource. Unfortunately, we can't change default executor. Here is example:
     new AsyncTask<Void, Void, String>() {  
       ...  
     }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);  

Another very useful method in new api is
     public static void execute(Runnable runnable) {
        AsyncTask.execute(runnable);
    }  

Thank you for attention.

3 comments:

  1. Why would anybody want to use a serial executor instead of a parallel executor? What would be the advantage of that?

    ReplyDelete
    Replies
    1. maybe I need sequential order of execution.

      Delete
  2. Awesome, this was a big help.

    It seems that serial is the default way async tasks execute nowadays (I believe it has changed since this was written).

    @Igor I guess that from a performance point of view, reusing threads is better as a rule. If you want to push to a thread, you can by executeOnExecutor().

    ReplyDelete