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

17

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

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

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

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

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

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

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

    
задан AndroDev 07.05.2012 в 12:38
источник
  • У меня есть одна и та же проблема, можете ли вы дать какое-либо предложение для моего вопроса стека stackoverflow.com/questions/17326931/... –  Karan Mavadhiya 10.07.2013 в 12:58

4 ответа

30

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 (тысячи задач добавляются в очередь с поддержкой массива).

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

    
ответ дан Roman Mazur 07.05.2012 в 13:04
источник
  • Не будет ли он вызывать исключение RejectedExecutionException (а не OutOfMemory), если он заполняет рабочую очередь. –  Jens 07.05.2012 в 13:11
  • Я буду на случай, если вы запустите их с PARALLEL_EXECUTOR. Но в случае последовательной используется ArrayDequeue. Отредактировал ответ. –  Roman Mazur 07.05.2012 в 13:15
  • Спасибо, Раман. Ваш ответ уточняет мои запросы относительно AsyncTask. –  AndroDev 08.05.2012 в 15:24
6

давайте погрузимся глубоко в файл Asynctask.java Android, чтобы понять его с точки зрения дизайнера и как он хорошо реализовал в нем шаблон разработки 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».

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

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

  2. метод 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 здесь.

    
ответ дан somenath mukhopadhyay 04.12.2013 в 02:14
источник
4

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

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

ответ дан kapand 07.05.2012 в 13:12
источник
  • Ваш ответ полезен. Благодарю. Я хочу знать еще одну вещь. Я запускаю эти 75000 задач в цикл for и не получаю никаких исключений (как вы сказали). Я вызываю task.execute (param) in для цикла, и я не вижу этого исключения. Тогда я не понимаю, когда это число 128 входит в картину? Означает, как я могу воспроизвести ваше исключение, если я запускаю более 128 задач? –  AndroDev 07.05.2012 в 13:37
  • Попробуйте использовать PARALLEL_EXECUTOR и executeOnExecutor (). Не вызывайте метод execute () по умолчанию. –  Roman Mazur 07.05.2012 в 13:55
  • В Android 3.x метод task.executeOnExecutor (Executor exec, Params ... params) позволяет использовать собственный исполнитель пула потоков и настраивать размер очереди замедленных заданий. –  kapand 07.05.2012 в 14:22
1
  1. Сколько AsyncTask можно запустить одновременно в приложении для Android?

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

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

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

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

    
ответ дан Jens 07.05.2012 в 13:04
источник
  • Спасибо за ваш ответ. Я хочу знать еще одну вещь. Как я могу получить исключение RejectExecutionException, когда я запускаю более 128 задач в своем приложении? –  AndroDev 07.05.2012 в 13:40
  • Существует жестко зафиксированный верхний предел из 128 потоков - пул потоков больше не запускается, и задача будет помещена в рабочую очередь (которая имеет максимальный размер 10). Если эта очередь по очереди заполнена, задача будет отклонена, а политика отклонения по умолчанию - это AbortPolicy, которая генерирует исключение RejectedExecutionException. –  Jens 07.05.2012 в 13:53