Написание блокирующего и неблокирующего кода с помощью сопрограмм

Одна из интересных особенностей Kotlin - сопрограммы. В разработке Android управление потоками всегда было немного сложным, потому что существуют ограничения на работу с вещами, связанными с пользовательским интерфейсом или основным потоком. И было обычной практикой использовать пользовательский интерфейс или основной поток для операций, связанных с пользовательским интерфейсом, и фоновые потоки для блокировки таких операций, как сетевые вызовы, операции с базой данных, тяжелые вычислительные задачи и т. Д.

Если управление потоками не выполняется должным образом, это оказывает огромное влияние на производительность приложения, приводя к ошибкам ANR, зависанию пользовательского интерфейса, сбоям и т. Д. Поэтому очень важно делать что-то в правильном потоке.

Управление потоками или многопоточность может быть достигнуто разными способами. Но Kotlin Coroutines предоставляют самое простое решение для обработки потоков. Сопрограммы - это не что иное, как легкие потоки. Они предоставляют нам простой способ синхронного и асинхронного программирования. Сопрограммы позволяют нам заменять обратные вызовы и создавать основную безопасность, не блокируя основной поток.

Посмотрим, как это сделать.

Примечание. Класс AsyncTask устарел в Android 11. Сопрограммы Kotlin теперь являются рекомендуемым решением для асинхронного кода.

Использование функций приостановки

Давайте разберемся в этом на простом примере получения сведений о пользователе из запроса API или локальной базы данных и отображения их пользователю. Если мы попытаемся получить детали через вызов API в основном потоке, он выдаст NetworkOnMainThreadException. Если мы извлечем данные из базы данных, эта операция заблокирует выполнение основным потоком своих операций до тех пор, пока данные не будут извлечены, что приведет к плохому взаимодействию с пользователем.

Мы можем просто сделать это, используя suspend функции в сопрограммах. suspend означает, что метод, связанный с этим модификатором, является синхронным и не будет возвращен немедленно. Связанный с ним метод будет приостановлен на некоторое время, а затем вернет результат, когда он станет доступным.

Давайте визуализируем это более наглядно:

Когда сопрограмма приостановлена, она не выполняется. Это приостановлено. И когда он возобновляется, он возобновляет работу с того места, на котором остановился. Когда мы вызываем функцию suspend в основном потоке, эта функция приостанавливается и выполняет свою работу в любом другом рабочем потоке. Как только это будет сделано, он возобновится с результатом, чтобы мы могли использовать результат в основном потоке.

Но есть некоторые ограничения на использование suspend функций. Их можно вызывать либо внутри построителя сопрограмм, либо из другой suspend функции.

Диспетчеры используются для указания, в каком потоке должна выполняться сопрограмма. Они похожи на планировщики в Rx. Мы можем указать, для каких диспетчеров мы хотим, чтобы запрос fetchUser API выполнялся (в нашем случае это Dispatchers.IO). Мы используем метод withContext, чтобы указать это:

После выполнения запроса API мы можем использовать результат в основном потоке, используя Dispatchers.Main. Доступны следующие диспетчеры:

  • Dispatchers.Default: Это диспетчер сопрограмм по умолчанию, который используется всеми разработчиками сопрограмм, такими как launch, async и т. Д., Если в их контексте не указан диспетчер или какой-либо другой ContinuationInterceptor. Он используется в таких сценариях, как выполнение интенсивной работы ЦП вне основного потока.
  • Dispatchers.IO: Этот диспетчер сопрограмм предназначен для блокирования операций ввода-вывода диска и сетевых операций. Дополнительные потоки в этом пуле создаются и закрываются по запросу.
  • Dispatchers.Main: диспетчер сопрограмм, который ограничен основным потоком , работающим с объектами пользовательского интерфейса. Доступ к этому свойству может вызвать IllegalStateException, если в пути к классам нет диспетчеров основного потока. Он используется в основном для получения результатов ответа от операций API, операций с базой данных и т. Д.

Используйте соответствующий диспетчер, исходя из ваших требований.

Что происходит под капотом

Компилятор Kotlin записывает обратные вызовы, когда вычисление может приостановиться. Сопрограммы вызывают эти обратные вызовы как продолжение . Продолжение - это не что иное, как общий интерфейс обратного вызова с некоторой дополнительной информацией в нем. Конечный автомат будет примерно таким:

Структурированный параллелизм

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

  1. Кто может отменить выполнение сопрограммы?
  2. Есть ли у сопрограммы жизненный цикл?
  3. У кого возникает исключение, когда сопрограмма дает сбой?

Структурированный параллелизм управляет этими вещами, вводя концепцию, называемую областью видимости.

Область сопрограмм

CoroutineScope - это интерфейс в пакете kotlinx.coroutines.

  1. Он отслеживает сопрограммы.
  2. Он дает возможность отменить сопрограмму.
  3. Он будет уведомлен всякий раз, когда произойдет сбой.

Область сопрограммы - это не что иное, как переменная, которую очень легко создать. Он не будет содержать ссылок на какие-либо тяжелые предметы. Поэтому всякий раз, когда мы хотим управлять жизненным циклом сопрограммы, нам нужно создать область видимости. Его легко создать следующим образом:

Теперь сопрограммы, созданные выше с помощью launch построителя сопрограмм, будут следовать жизненному циклу области. Если возникнут какие-либо исключения, об этом будет сообщено в область действия, чтобы мы могли их обработать. Во многих случаях, когда пользователи закрывают экран во время выполнения сетевого запроса, нам необходимо отменить все текущие запросы. Таким образом, в обратном вызове ViewModel, onCleared() или в обратном вызове действия onDestroy() мы можем отменить область действия, чтобы все сопрограммы прекратили свое выполнение.

Отмена прицела означает:

  1. Он отменяет все запущенные дочерние сопрограммы.
  2. Мы не можем запускать больше сопрограмм с этой областью видимости.

Обработка исключений области сопрограммы

Область также принимает задание в качестве параметра.

Задание можно использовать для определения жизненного цикла области видимости и сопрограммы. Если мы передадим задание в область видимости, оно будет обрабатывать исключения определенным образом. Когда с областью связано несколько дочерних элементов:

  • Если какой-либо из дочерних элементов потерпит неудачу, он распространит исключение на других дочерних элементов.
  • Когда сообщается об ошибке, область действия отменяет себя и распространяет исключение вверх.

Остановка других дочерних элементов и выдача исключения не идеальны в большинстве случаев сбоя, часто приводящего к сбою. В этих случаях нам нужно использовать SupervisorJob .

Когда мы используем SupervisorJob, он не останавливает выполнение других дочерних элементов в той же области. И когда сообщается о сбое, осциллограф ничего не делает. Поскольку исключение может распространяться вверх, нам, возможно, придется использовать try/catch блоков в нашем коде для безопасности.

Я напишу отдельную статью об обработке исключений сопрограмм, области видимости и т. Д.

Как создать сопрограмму

Мы можем создавать сопрограммы, используя доступные конструкторы сопрограмм. В основном мы используем два:

  1. launch
  2. async

запуск

launch{} - это конструктор сопрограмм, который создает новую сопрограмму "запустил и забыл". Функция launch создает сопрограмму и немедленно возвращается. Однако работа продолжается в фоновом пуле потоков. Он запускает и забывает сопрограмму. Обычно используется, когда возвращаемого значения не ожидается. Например, если мы хотим загрузить что-то на сервер и нас не волнует результат:

асинхронный

async{} - это построитель сопрограмм, который создает новую сопрограмму и используется в случае ожидаемого возвращаемого значения. Например, давайте рассмотрим приведенный выше пример получения пользовательских данных из вызова API:

async вернет отложенный объект. Мы вызываем await() для отложенного объекта, поэтому await будет ждать, приостановить выполнение сопрограммы до тех пор, пока async не завершит вычисление, и вернет значение сопрограммы.

Сравнение

launch и async не suspend функции. Они являются отправной точкой для сопрограмм. Мы можем рассматривать их как функции расширения поверх области видимости.

Выводы

  • Не отмечайте функцию suspend, если она не нужна.
  • Проверьте методы yield или ensureActive в тяжелых вычислительных операциях, таких как чтение файлов, чтобы всякий раз, когда сопрограмма отменяется и вызывается эта функция, она останавливает выполнение этой сопрограммы.
  • Обработка исключений. В противном случае это может привести к сбою приложения.

Резюме

Теперь вы должны понять, что такое сопрограммы и их основное использование. Мы узнаем больше об этих сопрограммах, областях видимости и обработке исключений в моих следующих статьях.

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

использованная литература