19

我想知道 AsyncTask 在内部是如何工作的。

我知道它使用Java Executor来执行操作,但仍然有一些我不理解的问题。像:

  1. Android 应用程序一次可以启动多少个 AsyncTask?
  2. 当我启动 10 个 AsyncTask 时,所有任务会同时运行还是一个接一个?

我曾尝试使用 75000 AsyncTask 进行测试。我没有遇到任何问题,似乎所有任务都将被推送到堆栈并一个接一个地运行。

此外,当我启动 100000 个 AsyncTasks 时,我开始收到 OutOfMemoryError。

那么一次可以运行的 AsyncTask 是否有任何限制?

注意:我已经在 SDK 4.0 上测试过这些

4

4 回答 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 源代码我们可以说池中的线程数可以在 range 范围内变化5..128

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

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

于 2012-05-07T11:04:25.970 回答
7

让我们深入了解 Android 的 Asynctask.java 文件,从设计者的角度来理解它,以及它是如何在其中很好地实现 Half Sync-Half Async 设计模式的。

在类的开头几行代码如下:

 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 阻塞队列文档中知道的那样,它实际上提供了一个线程安全的同步队列,实现了 FIFO 逻辑。

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

如果我们查看前几行,我们会知道 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);
    }

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 类。在这堂课中,我们有finalArrayDeque<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() 函数中,我们使用已在 dequeue 末尾插入的 Runnable 对象初始化 mActive。这个 Runnable 对象(它只是 mActive)然后在从线程池中取出的线程上执行。在那个线程中,“finally”块被执行。

现在有两种情况。

  1. 另一个 Asynctask 实例已创建,我们在执行第一个任务时调用它的 execute 方法。

  2. 在执行第一个任务时,在 Asynctask 的同一实例上第二次调用 execute 方法。

场景一:如果我们看一下Serial Executor的execute函数,我们会发现实际上我们新建了一个可运行线程(Say thread 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 模式的异步层一样,

mTasks.offer(new Runnable() {
....
}

部分代码在调用执行函数时创建一个新线程并将其推送到队列(mTasks)。它是绝对异步完成的,因为它在将任务插入队列的那一刻,函数返回。然后后台线程以同步方式执行任务。所以它类似于半同步 - 半异步模式。对?

然后在那个线程 t 中,我们运行 mActive 的 run 函数。但是由于它在 try 块中,finally 只会在该线程中的后台任务完成后才会执行。(记住 try 和 finally 都发生在 t 的上下文中)。在 finally 块中,当我们调用 scheduleNext 函数时,mActive 变为 NULL,因为我们已经清空了队列。但是,如果创建了同一个 Asynctask 的另一个实例并且我们在它们上调用 execute,这些 Asynctask 的执行函数不会被执行,因为执行前的同步关键字,也因为 SERIAL_EXECUTOR 是一个静态实例(因此同一个类的所有对象将共享同一个实例......这是一个类级锁定的例子)我的意思是同一个 Async 类的任何实例都不能抢占在线程 t 中运行的后台任务。即使线程被某些事件中断,再次调用 scheduleNext() 函数的 finally 块也会处理它。这一切都意味着只有一个活动线程运行任务。对于不同的任务,这个线程可能不一样,但一次只有一个线程会执行该任务。因此,只有在第一个任务完成时,后面的任务才会一个接一个地执行。这就是为什么它被称为 SerialExecutor。即使线程被某些事件中断,再次调用 scheduleNext() 函数的 finally 块也会处理它。这一切都意味着只有一个活动线程运行任务。对于不同的任务,这个线程可能不一样,但一次只有一个线程会执行该任务。因此,只有在第一个任务完成时,后面的任务才会一个接一个地执行。这就是为什么它被称为 SerialExecutor。即使线程被某些事件中断,再次调用 scheduleNext() 函数的 finally 块也会处理它。这一切都意味着只有一个活动线程运行任务。对于不同的任务,这个线程可能不一样,但一次只有一个线程会执行该任务。因此,只有在第一个任务完成时,后面的任务才会一个接一个地执行。这就是为什么它被称为 SerialExecutor。

场景二:在这种情况下,我们会得到一个异常错误。要了解为什么不能在同一个 Asynctask 对象上多次调用执行函数,请查看以下代码片段,该代码片段取自 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)");
            }
        }

从上面的代码片段可以清楚地看出,如果我们在任务处于运行状态时调用执行函数两次,它会抛出 IllegalStateException 说“无法执行任务:任务已经在运行。”。

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

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

于 2013-12-04T01:14:22.910 回答
4

AsyncTasks 内部有一个固定大小的队列,用于存储延迟任务。队列大小默认为 10。例如,如果您连续启动 15 个任务,则前 5 个将进入它们的doInBackground(),但其余的将在队列中等待空闲工作线程。随着前 5 个任务之一完成,从而释放工作线程,队列中的任务将开始执行。在这种情况下,最多 5 个任务将一起运行。

是的,一次可以运行多少个任务是有限制的。所以 AsyncTask 使用线程池执行器,工作线程的最大数量有限,延迟任务队列使用固定大小 10。工作线程的最大数量为 128。如果您尝试执行超过 138 个自定义任务,您的应用程序将抛出RejectedExecutionException

于 2012-05-07T11:12:01.400 回答
1
  1. Android 应用程序一次可以启动多少个 AsyncTask?

    AsyncTaskLinkedBlockingQueue容量为 10(在 ICS 和姜饼中)的 a 支持。因此,这实际上取决于您尝试开始多少任务以及完成它们需要多长时间 - 但绝对有可能耗尽队列的容量。

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

    同样,这取决于平台。姜饼和 ICS 中的最大池大小为 128 - 但 *默认行为* 在 2.3 和 4.0 之间更改 - 从默认情况下的并行到串行。如果你想在 ICS 上并行执行,你需要调用 [executeOnExecutor][1] 连同THREAD_POOL_EXECUTOR

尝试切换到并行执行器并使用 75 000 个任务向其发送垃圾邮件 - 串行 impl。有一个ArrayDeque没有容量上限的内部(OutOfMemoryExceptions ofc 除外)。

于 2012-05-07T11:04:34.913 回答