Ограничения потоков Android AsyncTask?

95

Я разрабатываю приложение, в котором мне нужно обновлять некоторую информацию каждый раз, когда пользователь входит в систему, я также использую базу данных в телефоне. Для всех этих операций (обновления, получение данных из базы данных и т. Д.) Я использую асинхронные задачи. До сих пор я не понимал, почему мне не следует их использовать, но недавно я испытал, что если я выполняю некоторые операции, некоторые из моих асинхронных задач просто останавливаются при предварительном выполнении и не переходят к doInBackground. Это было слишком странно, чтобы оставить все как есть, поэтому я разработал еще одно простое приложение, чтобы проверить, что не так. И, как ни странно, я получаю такое же поведение, когда общее количество асинхронных задач достигает 5, а 6-я останавливается при предварительном выполнении.

Есть ли у Android ограничение asyncTasks для Activity / App? Или это просто ошибка, и о ней нужно сообщить? Кто-нибудь испытывал ту же проблему и, возможно, нашел способ ее решения?

Вот код:

Просто создайте 5 таких потоков для работы в фоновом режиме:

private class LongAsync extends AsyncTask<String, Void, String>
{
    @Override
    protected void onPreExecute()
    {
        Log.d("TestBug","onPreExecute");
        isRunning = true;
    }

    @Override
    protected String doInBackground(String... params)
    {
        Log.d("TestBug","doInBackground");
        while (isRunning)
        {

        }
        return null;
    }

    @Override
    protected void onPostExecute(String result)
    {
        Log.d("TestBug","onPostExecute");
    }
}

А затем создайте эту ветку. Он войдет в preExecute и зависнет (в doInBackground не пойдет).

private class TestBug extends AsyncTask<String, Void, String>
{
    @Override
    protected void onPreExecute()
    {
        Log.d("TestBug","onPreExecute");

        waiting = new ProgressDialog(TestActivity.this);
        waiting.setMessage("Loading data");
        waiting.setIndeterminate(true);
        waiting.setCancelable(true);
        waiting.show();
    }

    @Override
    protected String doInBackground(String... params)
    {
        Log.d("TestBug","doInBackground");
        return null;
    }

    @Override
    protected void onPostExecute(String result)
    {
        waiting.cancel();
        Log.d("TestBug","onPostExecute");
    }
}
Миндаугас Свирскас
источник

Ответы:

207

Все AsyncTasks контролируются внутренне общим (статическим) ThreadPoolExecutor и LinkedBlockingQueue . Когда вы вызываете executeAsyncTask, ThreadPoolExecutorон выполнит его, когда он будет готов когда-нибудь в будущем.

«Когда я буду готов?» поведение a ThreadPoolExecutorконтролируется двумя параметрами: размером основного пула и максимальным размером пула . Если количество активных потоков меньше, чем размер основного пула, и поступает новое задание, исполнитель создаст новый поток и немедленно выполнит его. Если запущены потоки размером не менее основного пула, он попытается поставить задание в очередь и ждать, пока не появится свободный поток (т.е. пока не будет завершено другое задание). Если невозможно поставить задание в очередь (очередь может иметь максимальную емкость), он создаст новый поток (потоки до максимального размера пула) для выполнения заданий. Неосновные незанятые потоки в конечном итоге могут быть выведены из эксплуатации в соответствии с параметром тайм-аута keep-alive.

До Android 1.6 размер основного пула составлял 1, а максимальный размер пула - 10. Начиная с Android 1.6, размер основного пула равен 5, а максимальный размер пула равен 128. Размер очереди равен 10 в обоих случаях. Таймаут проверки активности составлял 10 секунд до 2.3 и 1 секунду с тех пор.

Учитывая все это, теперь становится ясно, почему AsyncTaskбудет казаться, что выполняется только 5/6 ваших задач. Шестая задача ставится в очередь до завершения одной из других задач. Это очень хорошая причина, по которой вы не должны использовать AsyncTasks для длительных операций - это предотвратит запуск других AsyncTasks.

Для полноты, если вы повторили упражнение с более чем 6 задачами (например, 30), вы увидите, что будет введено более 6 задач, doInBackgroundпоскольку очередь будет заполнена, а исполнитель будет вынужден создать больше рабочих потоков. Если вы продолжили выполнять длительную задачу, вы должны увидеть, что 20/30 стали активными, а 10 все еще находятся в очереди.

Антонит
источник
2
«Это очень веская причина, по которой вы не должны использовать AsyncTasks для длительных операций». Какова ваша рекомендация для этого сценария? Создавать новый поток вручную или создавать собственную службу исполнителя?
user123321 03
2
По сути, исполнители - это абстракция поверх потоков, что устраняет необходимость писать сложный код для управления ими. Это отделяет ваши задачи от того, как они должны выполняться. Если ваш код зависит только от исполнителя, то легко прозрачно изменить количество используемых потоков и т. Д. Я не могу придумать вескую причину для создания потоков самостоятельно, поскольку даже для простых задач объем работы с Исполнитель тот же, если не меньше.
Антонит 09
37
Обратите внимание, что начиная с Android 3.0+ количество одновременно выполняемых AsyncTasks по умолчанию было уменьшено до 1. Дополнительная информация: developer.android.com/reference/android/os/…
Киран
Вау, большое спасибо за отличный ответ. Наконец, у меня есть объяснение, почему мой код дает сбой так спорадически и загадочно.
Крис Найт,
@antonyt, Еще одно сомнение, Cancelled AsyncTasks, будет ли засчитываться количество AsyncTasks? т.е. засчитывается core pool sizeили maximum pool size?
nmxprime 01
9

У @antonyt есть правильный ответ, но если вы ищете простое решение, вы можете проверить Needle.

С его помощью вы можете определить собственный размер пула потоков, и, в отличие от него AsyncTask, он работает на всех версиях Android одинаково. С его помощью вы можете говорить такие вещи, как:

Needle.onBackgroundThread().withThreadPoolSize(3).execute(new UiRelatedTask<Integer>() {
   @Override
   protected Integer doWork() {
       int result = 1+2;
       return result;
   }

   @Override
   protected void thenDoUiRelatedWork(Integer result) {
       mSomeTextView.setText("result: " + result);
   }
});

или что-то вроде

Needle.onMainThread().execute(new Runnable() {
   @Override
   public void run() {
       // e.g. change one of the views
   }
}); 

Он может даже больше. Проверьте это на GitHub .

Жолт Сафрани
источник
последний коммит был 5 лет назад :(
LukaszTaraszka
5

Обновление : Начиная с API 19, размер пула основных потоков был изменен, чтобы отразить количество процессоров на устройстве, минимум 2 и максимум 4 при запуске, при увеличении до максимального значения CPU * 2 +1 - Ссылка

// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

Также обратите внимание, что хотя исполнитель AsyncTask по умолчанию является последовательным (выполняет одну задачу за раз и в том порядке, в котором они поступают), с методом

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params)

вы можете предоставить Executor для выполнения ваших задач. Вы можете предоставить THREAD_POOL_EXECUTOR внутренний исполнитель, но без сериализации задач, или вы даже можете создать свой собственный Executor и предоставить его здесь. Однако внимательно обратите внимание на предупреждение в Javadocs.

Предупреждение. Разрешение параллельного выполнения нескольких задач из пула потоков обычно не то, что нужно, потому что порядок их работы не определен. Например, если эти задачи используются для изменения какого-либо общего состояния (например, для записи файла из-за нажатия кнопки), нет никаких гарантий относительно порядка изменений. Без тщательной работы в редких случаях новая версия данных может быть перезаписана старой, что приведет к неявной потере данных и проблемам со стабильностью. Такие изменения лучше всего выполнять серийно; чтобы гарантировать сериализацию такой работы независимо от версии платформы, вы можете использовать эту функцию с SERIAL_EXECUTOR.

Еще одна вещь, которую следует отметить, заключается в том, что и платформа, предоставляющая Executors THREAD_POOL_EXECUTOR, и ее серийная версия SERIAL_EXECUTOR (которая по умолчанию для AsyncTask) являются статическими (конструкции уровня класса) и, следовательно, являются общими для всех экземпляров AsyncTask во всем процессе вашего приложения.

rnk
источник