Android中的AsyncTask是如何工作的?

19
我想了解AsyncTask的内部工作原理。
我知道它使用Java Executor执行操作,但仍有一些问题我不太明白,例如:
  1. 在Android应用程序中可以同时启动多少个AsyncTask?
  2. 当我启动10个AsyncTask时,所有任务都会同时运行还是一个接一个地运行?
我已经尝试使用75000个AsyncTask进行测试。我没有遇到任何问题,似乎所有任务都将被推送到堆栈并逐个运行。
另外,当我启动100000个AsyncTask时,我开始收到OutOfMemoryError错误。
那么,可以同时运行的AsyncTask数量是否有限制?
注意:我已在SDK 4.0上进行了测试。

我有同样的问题,你能给我的堆栈问题提供任何建议吗?http://stackoverflow.com/questions/17326931/download-bunch-of-files-from-server - Karan Mavadhiya
4个回答

33

AsyncTask有一段相当漫长的历史。

当它首次出现在Cupcake(1.5)中时,它使用一个附加线程(一个接一个)处理后台操作。 在Donut(1.6)中进行了更改,开始使用线程池。 直到池耗尽之前,可以同时处理操作。 在这种情况下,操作会被排队等待处理。

自从Honeycomb以来,默认行为已切换回使用单个工作线程(一个接一个处理)。 但是引入了新方法(executeOnExecutor),让您有可能并行运行任务(有两个不同的标准执行程序:SERIAL_EXECUTORTHREAD_POOL_EXECUTOR)。

任务排队的方式也取决于您使用的执行程序。 在并行执行程序的情况下,您受限于10个任务的限制(new LinkedBlockingQueue<Runnable>(10))。 在串行执行程序的情况下,您没有限制(new ArrayDeque<Runnable>())。

因此,您的任务如何处理取决于您如何运行它们以及您在哪个SDK版本上运行它们。 至于线程限制,我们没有任何保证,但是查看ICS源代码可以发现池中的线程数在范围5..128内变化。

当您使用默认的execute方法启动100000个任务时,将使用串行执行程序。 由于无法立即处理的任务会排队等待处理,因此会出现OutOfMemoryError(成千上万的任务被添加到数组支持的队列中)。

可以一次启动的确切任务数取决于您正在运行的设备的内存类别以及再次您使用的执行程序。


1
如果工作队列被填满了,它会抛出RejectedExecutionException而不是OutOfMemory吗? - Jens
1
如果您使用PARALLEL_EXECUTOR运行它们,我会这样做。但在串行执行时,将使用ArrayDequeue。已编辑答案。 - Roman Mazur
谢谢你,Raman。你的回答解决了我关于AsyncTask的疑问。 - AndroDev
LinkedBlockingQueue 在 Android KitKat 中作为 THREAD_POOL_EXECUTOR 的容量现在是 128。ArrayDeque 作为 SERIAL_EXECUTOR 仍然没有容量限制。 - Jeff Lockhart

7
让我们深入研究Android的Asynctask.java文件,从设计者的角度了解它如何很好地实现了半同步半异步设计模式。该类的开头几行代码如下所示:
在类的开头几行代码中:
 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());
        }
    };



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

    /**
    * 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);

第一个是ThreadFactory,其负责创建工作线程。该类的成员变量是到目前为止创建的线程数。当它创建一个工作线程时,这个数字就会增加1。

接下来是BlockingQueue。从Java的blockingqueue文档中可以知道,它实际上提供了一个线程安全的同步队列,实现先进先出的逻辑。

接下来是线程池执行器,它负责创建一个工作线程池,可以在需要时调用不同的任务来执行。

如果我们看一下前几行,我们就会知道Android将最大线程数限制为128(如private static final int MAXIMUM_POOL_SIZE = 128所示)。

现在下一个重要的类是SerialExecutor,定义如下:

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);
           }
       }
   }

下一个重要的两个函数在Asynctask中是:
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

and

public final AsyncTask<Params, Progress, Result> 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;
    }

从上述代码中可以看出,我们可以从Asynctask的exec函数中调用executeOnExecutor,并且在这种情况下它会使用默认执行程序。如果我们深入研究Asynctask的源代码,我们会发现这个默认执行程序实际上只是一个串行执行程序,其代码如上所示。

现在让我们深入了解SerialExecutor类。在这个类中,我们有最终的ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();

这实际上作为不同线程上的不同请求的序列化器。这是半同步半异步模式的一个例子。

现在让我们来看看串行执行程序是如何做到这一点的。请查看SerialExecutor代码的部分,如下所示:

 if (mActive == null) {
                scheduleNext();
            }

当Asynctask的execute方法第一次被调用时,该代码将在主线程上执行(因为mActive将被初始化为NULL),因此它将带我们进入scheduleNext()函数。 scheduleNext()函数编写如下:

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

在schedulenext()函数中,我们使用已经插入到队列末尾的Runnable对象来初始化mActive。然后将该Runnable对象(即mActive)在从线程池获取的线程上执行,在该线程中,"finally"块被执行。
现在有两种情况:
1. 在第一个任务正在执行时,另一个Asynctask实例已创建并调用了execute方法。 2. 当第一个任务正在执行时,同一个Asynctask实例第二次调用execute方法。
情况I:如果我们看一下Serial Executor的execute函数,我们会发现我们实际上为处理后台任务创建了一个新的可运行线程(假设是线程t)。请参阅以下代码片段-
 public synchronized void execute(final Runnable r) {
           mTasks.offer(new Runnable() {
               public void run() {
                   try {
                       r.run();
                   } finally {
                       scheduleNext();
                   }
               }
           });

正如从代码 mTasks.offer(new Runnable) 可以看出,每次调用 execute 函数都会创建一个新的工作线程。现在您可能已经能够发现 Half Sync - Half Async 模式与 SerialExecutor 的功能之间的相似之处了。然而,让我澄清一下疑惑。就像 Half Sync - Half Async 模式的异步层一样,SerialExecutor 也具有将任务提交到队列中并在后台线程上执行的功能。
mTasks.offer(new Runnable() {
....
}

代码的一部分在execute函数被调用的时候创建了一个新的线程,并将其推入队列(mTasks)。这是完全异步完成的,因为它在将任务插入队列的同时,函数就会返回。然后,后台线程以同步方式执行任务。所以这类似于Half Sync - Half Async模式。对吗?
然后,在那个线程t中,我们运行mActive的run函数。但是由于它在try块中,所以finally语句块只有在该线程中的后台任务完成后才会执行(请记住,try和finally都发生在t的上下文中)。当我们在finally块中调用scheduleNext函数时,由于我们已经清空了队列,mActive变成了NULL。但是,如果创建了同一个Asynctask的另一个实例并对它们调用execute,则由于在execute之前的同步关键字以及SERIAL_EXECUTOR是静态实例(因此所有相同类的对象将共享同一个实例...这是类级锁定的一个示例),这些Asynctask的execute函数将不会被执行,也就是说没有同一Async类的实例可以抢占在线程t中正在运行的后台任务。即使线程被某些事件中断,再次调用scheduleNext()函数的finally块也会处理它。这意味着只有一个活动线程在运行任务。这个线程对于不同的任务可能并不相同,但是一次只有一个线程会执行任务。因此,后续任务将在第一个任务完成后依次执行。这就是为什么被称为SerialExecutor的原因。
情景II:在这种情况下,我们将得到一个异常错误。要了解为什么无法在同一个Asynctask对象上调用多次execute函数,请查看从Asynctask.java的executorOnExecute函数中获取的以下代码片段,尤其是下面提到的部分:
 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)");
            }
        }

从上面的代码片段可以看出,如果在任务处于运行状态时调用execute函数两次,它会抛出一个IllegalStateException,显示“无法执行任务:任务已在运行中。”。

如果我们希望多个任务并行执行,我们需要调用execOnExecutor并传递Asynctask.THREAD_POOL_EXECUTOR(或者可能是用户定义的THREAD_POOL作为exec参数)。

你可以在这里阅读我关于Asynctask内部的讨论


4
AsyncTasks内部有一个固定大小的队列来存储延迟任务。默认情况下,队列大小为10。例如,如果您按顺序启动15个任务,则前5个将进入其doInBackground()方法,但其余的任务将在队列中等待空闲的工作线程。当前5个任务中的一个完成并释放了工作线程时,队列中的一个任务将开始执行。在这种情况下,最多可以同时运行5个任务。
是的,有一定数量的任务可以同时运行的限制。因此,AsyncTask使用带有有限的工作线程最大数和延迟任务队列使用固定大小10的线程池执行器。工作线程的最大数为128。如果尝试执行超过138个自定义任务,则应用程序会抛出RejectedExecutionException异常。

你的回答很有帮助。谢谢。我还想知道一件事情。我在for循环中启动了这75000个任务,并且没有收到任何异常(正如你所说)。我在for循环中调用task.execute(param),并且没有看到那个异常。那么我不明白这个数字128是什么意思?如果我运行超过128个任务,我该如何重现你的异常? - AndroDev
尝试使用PARALLEL_EXECUTOR和executeOnExecutor()。不要调用默认的execute()方法。 - Roman Mazur
在Android 3.x中,方法task.executeOnExecutor(Executor exec, Params... params)允许使用自定义线程池执行器,并配置延迟任务队列的大小。 - kapandron

1
  1. 在Android应用程序中,可以同时启动多少个AsyncTask?

    AsyncTask由容量为10的LinkedBlockingQueue支持(在ICS和gingerbread中)。因此,它实际上取决于您要启动多少任务以及它们需要多长时间才能完成 - 但肯定有可能耗尽队列的容量。

  2. 当我启动10个AsyncTask时,所有任务都会同时运行还是一个接一个运行?

    同样,这取决于平台。在gingerbread和ICS中,最大池大小均为128 - 但是默认行为在2.3和4.0之间发生了变化 - 从默认并行更改为串行。如果您想在ICS上并行执行,则需要调用[executeOnExecutor][1]与THREAD_POOL_EXECUTOR一起使用。

尝试切换到并行执行器并向其发送75,000个任务 - 串行实现具有没有上限的内部ArrayDeque(当然除了OutOfMemoryExceptions)。


谢谢您的回答。我还想知道一件事。当我在我的应用程序中启动超过128个任务时,如何获得RejectExecutionException? - AndroDev
线程池的硬编码上限为128个线程,线程池不会启动更多线程,任务将被放入工作队列(最大大小为10)。如果该队列也已满,则任务将被拒绝,而默认的拒绝执行策略是“AbortPolicy”,它会抛出一个RejectedExecutionException异常。 - Jens

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接