Подтвердить что ты не робот

Как работает AsyncTask на Android

Я хочу знать, как работает AsyncTask внутри.

Я знаю, что он использует Java Executor для выполнения операций, но все же некоторые из вопросов, которые я не понимаю. Как:

  • Сколько AsyncTask можно запустить одновременно в приложении для Android?
  • Когда я запустил 10 AsyncTask, будут ли выполняться все задачи одновременно или один за другим?

Я попытался с 75000 AsyncTask протестировать то же самое. У меня нет проблем и похоже, что все задачи будут перенесены в стек и будут запускаться один за другим.

Также, когда я запускаю 100000 AsyncTasks, я начинаю получать OutOfMemoryError.

Итак, существует ли какой-либо предел для AsyncTask, который может запускаться за раз?

Примечание. Я тестировал их на SDK 4.0

4b9b3361

Ответ 1

AsyncTask имеет довольно длинную историю.

Когда он впервые появился в Cupcake (1.5), он обработал фоновые операции одним дополнительным потоком (один за другим). В Donut (1.6) он был изменен, так что пул ниток начал использоваться. И операции могут обрабатываться одновременно до тех пор, пока пул не будет исчерпан. В этом случае операции были заключены в очередь.

Так как поведение по умолчанию Honeycomb переключается на использование одного рабочего потока (по одной обработке). Но вводится новый метод (executeOnExecutor), чтобы дать вам возможность запускать одновременные задания, если хотите (существуют два разных стандартных исполнителей: SERIAL_EXECUTOR и THREAD_POOL_EXECUTOR).

Способ выделения заданий зависит также от того, какой исполнитель вы используете. В случае параллельного вы ограничены пределом 10 (new LinkedBlockingQueue<Runnable>(10)). В случае последовательного вы не ограничены (new ArrayDeque<Runnable>()).

Таким образом, способ обработки ваших задач зависит от того, как вы запускаете их и какую версию SDK вы запускаете. Что касается ограничений по потокам, мы не гарантируем их, но, глядя на исходный код ICS, мы можем сказать, что количество потоков в пуле может варьироваться в диапазоне 5..128.

При запуске 100000 с использованием метода execute по умолчанию используется последовательный исполнитель. Так как задачи, которые не могут быть немедленно обработаны, вы ставите в очередь, вы получаете OutOfMemoryError (тысячи задач добавляются в очередь с поддержкой массива).

Точное количество задач, которые вы можете запустить сразу, зависит от класса памяти устройства, на котором вы работаете, и, опять же, с исполнителем, который вы используете.

Ответ 2

давайте погрузимся глубоко в файл Androids 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 (как видно из частного статического 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;
    }

Как видно из приведенного выше кода, мы можем вызвать executeOnExecutor из функции exec из Asynctask, и в этом случае он принимает исполнитель по умолчанию. Если мы выкопаем исходный код Asynctask, мы обнаружим, что этот исполнитель по умолчанию - это не что иное, как последовательный исполнитель, код которого приведен выше.

Теперь давайте вникаем в класс SerialExecutor. В этом классе мы имеем окончательный ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();.

Это фактически работает как сериализатор различных запросов в разных потоках. Это пример Half Half Sync Half Async.

Теперь рассмотрим, как это делает последовательный исполнитель. Пожалуйста, посмотрите на часть кода SerialExecutor, которая написана как

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

Итак, когда вызов сначала вызывается в Asynctask, этот код выполняется в основном потоке (поскольку значение mActive будет инициализировано значением NULL), и, следовательно, это приведет нас к функции scheduleNext(). Функция ScheduleNext() была записана следующим образом:

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

Итак, в функции schedulenext() мы инициализируем mActive с помощью объекта Runnable, который мы уже вставили в конце dequeue. Этот объект Runnable (который не что иное, как mActive) затем выполняется в потоке, взятом из threadpool. В этом потоке выполняется блок "finally".

Теперь есть два сценария.

  • был создан еще один экземпляр Asynctask, и мы вызываем метод execute на нем при выполнении первой задачи.

  • метод execute вызывается во второй раз в том же экземпляре Asynctask, когда выполняется первая задача.

Сценарий I: если мы посмотрим на функцию выполнения Serial Executor, мы обнаружим, что мы фактически создаем новый runnable thread (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). Это делается абсолютно асинхронно, так как момент, когда он вставляет задачу в очередь, возвращает функцию. И тогда фоновый поток выполняет задачу синхронно. Таким образом, он похож на Half Sync - Half Async. Правильно?

Затем внутри этого потока t мы запускаем функцию запуска mActive. Но, как и в блоке try, окончательно будет выполняться только после завершения фоновой задачи в этом потоке. (Помните обе попытки и, наконец, происходит в контексте t). Внутри блока finally, когда мы вызываем функцию scheduleNext, mActive становится NULL, потому что мы уже опустошили очередь. Однако, если создается другой экземпляр той же Asynctask и мы вызываем execute на них, функция выполнения этих Asynctask не будет выполнена из-за ключевого слова синхронизации перед выполнением, а также потому, что SERIAL_EXECUTOR является статическим экземпляром (следовательно, все объекты тот же класс будет совместно использовать один и тот же экземпляр... его пример блокировки на уровне классов). Я имею в виду, что ни один экземпляр того же класса Async не может вытеснить фоновое задание, выполняемое в потоке t. и даже если поток прерывается некоторыми событиями, блок finally, который снова вызывает функцию scheduleNext(), позаботится об этом. Все это означает, что будет выполняться только один активный поток, выполняющий задачу. этот поток не может быть одинаковым для разных задач, но только один поток за раз выполнит задачу. поэтому последующие задачи будут выполняться один за другим только после завершения первой задачи. вот почему он называется SerialExecutor.

Сценарий II: В этом случае мы получим ошибку исключения. Чтобы понять, почему функция execute не может быть вызвана более одного раза на одном и том же объекте Asynctask, ознакомьтесь с нижеприведенным фрагментом кода, взятым из функции executorOnExecute Asynctask.java, особенно в приведенной ниже части:

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

AS из приведенного выше фрагмента кода становится ясным, что если мы дважды вызываем функцию выполнения, когда задача находится в текущем состоянии, она выдает исключение IllegalStateException "Невозможно выполнить задачу: задача уже запущена".

если мы хотим, чтобы несколько задач выполнялись параллельно, нам нужно вызвать execOnExecutor, передающий Asynctask.THREAD_POOL_EXECUTOR (или, возможно, пользовательский параметр THREAD_POOL в качестве параметра exec.

Вы можете прочитать мое обсуждение об внутренних функциях Asynctask здесь.

Ответ 3

AsyncTasks имеет внутреннюю папку с фиксированным размером для хранения отложенных задач. Размер очереди по умолчанию равен 10. Например, если вы запускаете 15 своих задач в строке, то первые 5 будут вводить их doInBackground(), а остальные будут ждать в очереди для бесплатного рабочего потока. Как один из первых 5 отделов и, таким образом, освобождает рабочий поток, задача из очереди начнет выполнение. В этом случае не более 5 задач будут выполняться вместе.

Да, существует ограничение на количество задач, которые могут быть запущены за один раз. Таким образом, AsyncTask использует исполнитель пула потоков с ограниченным максимальным количеством рабочих потоков, а очередь с задержками заданий использует фиксированный размер 10. Максимальное количество рабочих потоков - 128. Если вы попытаетесь выполнить более 138 пользовательских задач, ваше приложение будет бросать RejectedExecutionException.

Ответ 4

  • Сколько AsyncTask можно запустить одновременно в приложении для Android?

    AsyncTask поддерживается LinkedBlockingQueue с емкостью 10 (в ICS и пряниках). Таким образом, это действительно зависит от того, сколько задач вы пытаетесь запустить и как долго они завершатся, но это, безусловно, возможно исчерпать пропускную способность очереди.

  • Когда я запустил 10 AsyncTask, будут ли выполняться все задачи одновременно или один за другим?

    Опять же, это зависит от платформы. Максимальный размер пула - 128 в обоих имбирных и ICS - но поведение * по умолчанию * изменилось между 2,3 и 4.0 - от параллельного по умолчанию до серийного. Если вы хотите выполнить параллель в ICS, вам необходимо вызвать [executeOnExecutor] [1] в сочетании с THREAD_POOL_EXECUTOR

Попробуйте переключиться на параллельный исполнитель и спам его с 75 000 задач - последовательный impl. имеет внутренний ArrayDeque, который не имеет ограничений верхней емкости (кроме OutOfMemoryExceptions ofc).