Работа с многопоточностью в Android [Перевод]
Введение
Эта статья является переводом заметки
Каждый разработчик, в том или ином виде, сталкивался с необходимостью работы с потоками в их программе. Обычно, от потока требуется три вещи: стартовать, выполнить какую-то задачу, уничтожиться.
Такое поведение подходит для небольших задач, но не подходит для длительных по времени задач, когда необходима долгая работа.
Поскольку поток умирает после выполнения необходимой части работы, часто требуется создавать циклы цель которых только в поддержании работоспособности потока. Но должна также сохраняться возможность остановить поток, когда это необходимо.
К тому же иногда требуется некоторая очередь, обрабатываемая в цикле. Также нам может понадобиться еще один поток, который бы добавлял задачи в очередь потоков, для выполнения.
Выполнения всего комплекса операций, работа с состояниями потоков вызывает множество непределенностей. Но все эти возможности реализованы в Android в виде откдельных классов.
Android Поток
Когда приложение запускается вместе с ним создается обычный Linux-процесс. Внутри системы это создает поток выполнения для этого приложения, называемый основным потоком или UI-потоком (интерфейсным потоком). Основной поток по сути представляет собой обработчик потоков. Основной поток отвечает за обработку событий происходящий в приложении, такие как, например, коллбэки связанные с жизненным циклом элементов приложения или коллбэки связанные с событиями ввода. Также возможна обработка событий связанных с другими приложениями.
Любой блок кода, который необходимо запустить должен быть размещен в очереди на выполнение, после чего он будет выполнен в основном потоке. Поскольку основной поток берет на себя столько различных операций, длительные по времени операции лучше проводить в других потоках, что бы не задерживать основной. Важно избегать использование основного потока для длительных операций, что бы не ввести приложение в состояние ПНО (Приложение не отвечает - Application Not Responce).
Сетевые операции или запросы к базам данных, загрузка каких-то компонентов или другие схожие операции могут вызвать состояние блокирования основного потока приложения. Это значит, что пока некоторый длительный процесс не завершиться, пользовательский интерфейс, отрисовывающийся в основном потоке, также не будет отвечать. Что бы избегать таких ситуаций, такие операции выносят в отдельные потоки. Это значит, что они выполнятся асинхронно от пользовательского интерфейса.
Android предлает несколько вариантов создания и управления потоками, а также множество сторонних библиотек, которые делают управление потоками проще. Каждый вид потоков предназначен для какой-то определенной цели и выбор правильного потока, соответствующего задаче, очень важен.
Доступны следующие классы-потоки:
- AsyncTask: Помогает работать с UI-потоком
- HandlerThread: Поток для обратных вызовов
- ThreadPoolExecutor: Запускает множество параллельных потоков
- IntentService: Помогает получить намерение (intent) от UI-потока
AsyncTask
AsyncTask позволяет проще работать с интерфейсом из другого потока. Этот класс позволяет выполнять фоновые операции в отдельном потоке и отображать их результаты в потоке интерфейса без использования дополнительных обработчиков.
AsyncTask проектировался как класс-помощник для Thread и Handler и не является универсальным потоком для любых задач. AsyncTask, в идеале, необходимо использовать для быстрых операций, которые слишком тяжелы для основного потока (максимум несколько секунд). Если необходимо выполнять какую-то опрацию в потоке длительное время рекомендуется использовать различные АПИ из пакета java.util.concurrent
, такие как Executor
, ThreadPoolExeutor
, FutureTask
.
Когда асинхронная задача обрабатываются, выполняются следующие четыре шага:
onPreExecute()
: Вызывается в потоке пользовательского интерфейса, перед выполнением основной задачи. Обычно используется для того что бы выполнить что-то перед запуском процесса вычислений, например, показывает уведомление о начале процесса в пользовательском интерфейсе.doInBackground(Params...)
: Вызывается в фоновом потоке, послеonPreExecute()
. Этот шаг выполняет фоновые вычисления, которые могут длиться продолжнительное время. Параметры асинхронной задачи передаются на этом шаге. Результат вычислений вызвращается на этом шаге и отправляется вonPostExecute()
. На этом шаге также можно использоватьpublishProgress(..)
для уведомления других потоков о статусе выполнения задачи.onProgressUpdate(Progress…)
: Вызывается в потоке пользовательского интерфейса, после вызоваpublishProgress(..)
. Этот метод используется для показа прогресса процесса в любой форме в пользовательском интерфейсе, пока фоновая задача выполняется. Например, это может быть использовано для анимации полосы прогресса или отображения в лог-файлах.onPostExecute(Result)
: Вызывается в потоке пользовательского интерфейса, после того как фоновая задача завершится. Результат вычислений может быть передан в этот шаг, как параметр.
Выполнение задачи может быть остановлено с любой момент при помощи вызова cancel(boolean...)
. Перед вызовом отмены выполнения потока, нужно убедиться, что поток запущен.
Реализация
private class AsyncTaskRunner extends AsyncTask<String, String, String> {
@Override
protected void onPreExecute() {
progressDialog.show();
}
@Override
protected String doInBackground(String... params) {
doSomething();
publishProgress("Sleeping..."); // вызывывает onProgressUpdate()
return resp;
}
@Override protected void onPostExecute(String result) {
// результат выполнения долгой операции
progressDialog.dismiss();
updateUIWithResult() ;
}
@Override
protected void onProgressUpdate(String... text) {
updateProgressUI();
}
}
Когда использовать AsyncTask
AsyncTask практически идеален в тех ситуациях, когда нам нужно выполнить недлительную операцию и быстро вернуть результат, часто обновляя пользовательский интерфейс.
Однако, асинхронные задачи, прекращают свое выполнение, когда прекращает жизненный цикл активность/фрагмент. Иногда, даже поворот экрана может привести к прерыванию выполнения задачи.
Порядок выполнения
По-умолчанию, все созданные асинхронные задачи, будут работать из одного потока и выполняться последовательно в одной очереди сообщений. Иногда такой подход может повлиять на выполняемые задачи. Если нам необходимо параллельное выполнение, можно использовать THREAD_POOL_EXECUTOR
(прим. AsyncTask.THREAD_POOL_EXECUTOR
).
Поток-обработчик
Поток-обработчик это подкласс обычного класса потока в Java. Поток-обработчик предназначен для длительных операций, которые он может извлекать из очереди и обрабатывать. Этот класс также тесно связан с комбинацией Android-примитивов, таких как:
Looper
: сохраняет поток живым и помещает его в очередь на выполнениеMessageQueue
: класс хранящий очередь сообщений для передачиLooper
Handler
: позволяет отправлять и обрабатывать сообщения объектам связанным с потокомMessageQueue
Вся эта структура классов позволяет сохранять запущеннымив фоновом режиме задачи и отправлять на исполнение все новые и новые задачи, пока мы не уничтожим поток.
Потоки-обработчики работают вне жизненного цикла активности, так что необходимо помнить о возможной утечке памяти при раоте с ним.
Существует два способа создания потока-перехватчика:
-
Создать новый поток-обработчик и получить для него повторитель (
Looper
). Теперь, создаем новый обработчик, связываем его с повторителем и отправляем через обработчик задачи в очередь на исполнение. -
Расширить поток-обработчик созданием
CustomHandlerThread
. После создания класса, необходимо задать для него обработчик. Такой подход применятся в том случае, если задача, которой будет заниматься поток известна заранее. Например, создание пользовательского класса для загрузки изображений или выполнение сетевого запроса.
HandlerThread handlerThread = new HandlerThread("TesHandlerThread");
handlerThread.start();
Looper looper = handlerThread.getLooper();
Handler handler = new Handler(looper);
handler.post(new Runnable(){…});
Когда поток-обработчик создан, нужно не забыть о настройке его приоритета, потому что ЦП может обрабатывать ограниченное число потоков параллельно, так что нестройка приоритета может помочь системе узнать правильный способ менеджемента потоков вашего приложения.
Заметка: вызывайте
handlerThread.quit()
, когда фоновая задача завершена или методonDestroy()
Мы можем отправлять данные в пользовательский интерфейс используя локальную доставку или создавая обработчик связанный с основным потоком.
Handler mainHandler = new Handler(context.getMainLooper());
mainHandler.post(myRunnable);
## Когда использовать поток-обработчик
Поток-обработчик хорошее решение для длительных задач, которые не требуют частого обновления пользовательского интерфейса.
# Потоковое множество
## Что такое потоковое множество?
Потоковое множество базируется на идее некоторого количества потоков, которые работают в фоне, ожидая задач. Задача, которая приходит в эти потоки выполняется параллельно во всех потоках. Так как процесс выполнения не параллелен, нужно позаботиться о потокобезопаности. Потоковое множество предназначено для решения двух основных проблем:
- улучшение производительности при выполнении большого количества аинхронных задач, благодаря уменьшению затрат на порождение потоков
- как средство ограничения и управления ресурсами при выполнении множества задач.
Рассмотрим пример: имеется около 40BMP для декодирования, каждая картинка, в среднем, декодируется 4ms, и если запустить эту задачу в одном потоке, всего это займет около 160ms.
Однако, если запустить эту же задачу в 10 потоках, каждому нужно будет декодировать всего 4 картинки. Таким образом, общее время для этой задчи будет всего около 16ms.
Проблема в данном случае состоит в том, как распределить и передать задачу каждому потоку, как управлять результатами этой работы. Это сложная, комплексная проблема. В таком случае пригодится ThreadPoolExecutor
.
## Что такое ThreadPoolExecutor
?
Класс ThreadPoolExecutor
расширяет AbstractExecutorService
. Этот класс управляет множеством подчиненных потоков:
- раздает задачи каждому потоку
- сохраняет их работоспособность
- завершает потоки, если это необходимо
Внутри поддерживается очередь задач, из котого забирается задача и назначается освободившемуся потоку из множества потоков.
## Интерфейс Runnable
в множестве потоков
Этот интерфейс позволяет классу реализовывать поведение потока. Проще говоря: этот интерфейс говорит о том, что класс является командой или задачей на исполнение.
Runnable mRunnable = new Runnable() {
@Override
public void run() {
// Do some work
}
};
## Исполнитель
Исполнитель(Executor
) это интерфейс используемый для отделения исполняемой задачи от исполнителя. Это некоторый объект, который исполняет Runnable
.
Executor mExecutor = Executors.newSingleThreadExecutor(); mExecutor.execute(mRunnable);
Исполнитель-сервис
Этот класс (ExecutorService
) позволяет запускать задачи асинхронно.
ExecutorService mExecutorService = Executors.newFixedThreadPool(10); mExecutorService.execute(mRunnable);
Исполнитель множества потоков
Это ExecutorService
, который назначает задания множеству потоков.
Нужно помнить о том, что больше потоков н всегда лучше, потому что ЦП может выполнять только определенное количество потоков параллельно. Как только это число будет превышено, процессору необходимо будет приостановить вычисления и выполнить несколько дорогих вычислительных операций по определению того, какие потоки исполнять приоритетнее.
Когда мы создаем ThreadPoolExecutor
, мы определяем число начальных потоков и максимальное количество потоков. Когда будет расти количество задач, будет работать больше потоков из множества.
Обычно, рекомендуется аллоцирвоать при старте множество столько минимальных потоков, сколько ядер на устройстве
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
Заметка: эта команда не всегда возвращает количество аппаратных ядер на устройстве. Возможно, что некоторые ядра отключены для экономии заряда или недоступны и т.д.
ThreadPoolExecutor(
int corePoolSize, // начальный размер множества
int maximumPoolSize, // максимальное количество потоков
long keepAliveTime, // количество времени ожидаемое перед остановкой потока
TimeUnit unit // задаем единицу измерения для keepAliveTime
BlockingQueue<Runnable> workQueue) // объект очереди сообщений
Что означают эти параметры?
corePoolSize
: минимальное число потоков сохраняемое в множестве потоков. Изначально, в множестве нуль потоков. Но по мере добавления задач в очередь, новые потоки будут аллоцированы. Если потоков меньше чем задач, то Исполнитель предпочтет создать новый поток, а не ставить задачу в очередь.maximumPoolSize
: макиальное число потоков, разрешенное в множестве. Если это число превышаетcorePoolSize
и текущее число потоков больше чемcorePoolSize
, то новые потоки будут создаваться в случае, если очередь сообщений будет заполнена.keepAliveTime
: когда число потоков больше чем изначально указанное число, то ожидающие потоки (неактивные в даннный момент), не получившие по истечению указанного времени, определяемого этим параметром, будут завершены.unit
: единица измерений времени для предыдущего параметра.workQueue
: очередь сообщений, которая будет содержать задачи для выполнения. Обычно этоBlockingQueue
.
Когда использовать множество потоков?
ThreadPoolExecutor
мощный инструмент по работа с потоками, который используется при большом количестве задач, выполняемых параллельно - поддерживается добавление задач в очередь, отмена задач, приоретизация задач.
IntentService
IntentService
подкласс подчиненный классу Service
. Так что, что бы разобраться с сервисом намерений, нужно рассмотреть класс Service
.
Сервис это важный компонент в архитектуре приложений Android. Иногда требуется выполнить какую-то задачу уже после того, как приложение было закрыто. В этом случае сервис будет самым подходящим вариантом. Сервис запускается и останавливается методами startService ()
/ stopService ()
и запускается в фоне в течение длительного времени. Так же сервис может остановить сам себя вызвав метод stopSelf()
.
Рассмотрим методы, которые нужно переопределить для работы с сервисом:
onCreate()
: вызовется один раз, пока сервис не остановят
onStartCommand()
: эта функция вызовется после onCreate()
в первый запуск сервиса, но может быть также косвенно вызвана из startService()
onDestroy()
: вызовется при остановке сервиса
Нормальная схема состояний работы сервиса:
onCreate() -> onStartCommand() -> onDestroy()
IntentService
работает схожим образом. Стартует из основного потока при вызове метода startService()
. Обрабатывается каждое намерение переданное в onHandleIntent()
(такой способ используется чаще, чем запуск черех onStartCommand()
). Запускается обработчик задачи и работает пока задача не будет завершена и остановит сам себя по завершению. Для использования необходимо расширить базовый класс IntentService
и внутри реализовать onHandleIntent()
.
Заметка:
IntentService
запускается в одном потоке, пока сервис работает в основном потоке. Только одна задача обрабатывается за один запуск.
IntentService
все еще ограничен всеми особенностями фонового исполнения для потоков. В большинстве задач, лучше использовать JobIntentService
, если нужно работать с Android 8.0 и выше.
Когда использовать IntentService
IntentService
хорошо подходит для асинхронных запросов по требованию. Это хорошее решение в случае, если не требуется обрабатывать множество запросов паралллельно.