Изучите основные концепции и использование Kotlin Coroutines.

Что такое сопрограммы

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

Как импортировать Kotlin Coroutines в Android?

Согласно репозиторию Kotlin Coroutines на Github, нам необходимо импортировать kotlinx-coroutines-core и kotlinx-coroutines-android (эта библиотека поддерживает основной поток Android так же, как библиотека io.reactivex.rxjava2:rxandroid для RxJava, а также гарантирует, что неперехваченные исключения могут быть зарегистрированы до сбоя приложения Android.). Кроме того, если вы используете RxJava в своем проекте, добавьте kotlinx-coroutines-rx2 для использования сопрограмм с RxJava. Эта библиотека помогает переносить RxJava на Coroutines.

Импортируйте их в свой проект, добавив следующий код в app/build.gradle.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.2"

и добавьте последнюю версию Kotlin в свой root/build.gradle.

buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        jcenter()
        ...
    }
    ...
}

Ладно, вроде предварительные настройки сделаны. Начнем сегодняшнюю тему.

Оглавление

  1. Приостановка функций
  2. Область действия сопрограммы
    (1) Область действия сопрограммы
    (2) Основная область действия
    (3) Глобальная область действия
  3. Контекст сопрограммы
    (1) Диспетчеры
    (2) CoroutineExceptionHandler
    (3) Задание
    - (3.1) Иерархии родитель-потомок
    -
    (3.2) Работа супервизора vs Работа
  4. Построитель сопрограмм
    (1) запуск
    (2) async
  5. Тело сопрограммы

Основы сопрограмм

Во-первых, вот как в основном выглядят сопрограммы:

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

1. Приостановка функций

В Kotlin Coroutines есть специальная функция suspending functions, которую мы можем объявить с помощью ключевого слова suspend. Приостановка функций может приостановить выполнение сопрограммы, что означает, что она будет ждать возобновления приостановки функций. Поскольку этот пост посвящен базовой концепции сопрограмм, мы обсудим более подробную информацию о приостановке функций в этом посте.

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

2. CoroutineScope

Определяет область действия новых сопрограмм. Каждый конструктор сопрограмм является расширением CoroutineScope и наследует его coroutineContext для автоматического распространения как элементов контекста, так и отмены.

Все сопрограммы выполняются внутри CoroutineScope , и он принимает CoroutineContext (я расскажу об этом позже) в качестве параметра. Мы можем использовать несколько прицелов:

(1) CoroutineScope
Создает область действия с пользовательским CoroutineContext. Например, чтобы определить поток, родительское задание и обработчик исключений по нашей необходимости.

CoroutineScope(Dispatchers.Main + job + exceptionHandler).launch {
    ...
}

(2) MainScope
Создает основную область для компонентов пользовательского интерфейса. Он выполняется в основном потоке с SupervisorJob(), что означает, что сбой одного из его дочерних заданий не повлияет на другие.

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

(3) GlobalScope
Это область, которая не привязана к какой-либо работе. Он используется для запуска сопрограмм верхнего уровня, которые работают в течение всего срока службы приложения и не отменяются преждевременно.

3. CoroutineContext

Сопрограмма всегда выполняется в некотором контексте, представленном значением типа CoroutineContext. CoroutineContext - это набор элементов для определения политики потоковой передачи, обработчика исключений, управления временем жизни сопрограммы и т. Д. Мы можем использовать оператор плюс для объединения элементов CoroutineContext.

Есть три наиболее важных контекста Coroutine - Dispatchers, CoroutineExceptionHandler и Job.

(1) Диспетчеры
Определяет, какой поток запускает сопрограмму. Сопрограмма может переключать Диспетчера в любое время с помощью withContext().

  • Dispatchers.Default:
    Использует общий фоновый пул потоков. По умолчанию максимальное количество потоков, используемых этим диспетчером, равно количеству ядер ЦП, но не менее двух. Нить будет похожа на Thread[DefaultDispatcher-worker-2,5,main].
  • Dispatchers.IO:
    Делит потоки с Dispatchers.Default, но количество потоков ограничено kotlinx.coroutines.io.parallelism. По умолчанию установлено ограничение в 64 потока или количество ядра (в зависимости от того, что больше). Поток будет похож на Thread[DefaultDispatcher-worker-1,5,main], похоже, такой же, как на Dispatchers.Default.
  • Dispatchers.Main:
    Равно основному потоку Android. Нить была бы похожа на Thread[main,5,main].
  • Dispatchers.Unconfined:
    Диспетчер сопрограмм, не ограниченный каким-либо конкретным потоком. Сопрограмма сначала выполняется в текущем потоке и позволяет ей возобновить работу в любом потоке, который используется соответствующей функцией приостановки.

(2) CoroutineExceptionHandler
Обрабатывает неперехваченные исключения.

Обычно неперехваченные исключения могут быть результатом только сопрограмм, созданных с помощью построителя launch. Сопрограмма, созданная с использованием async, всегда улавливает все свои исключения и представляет их в итоговом объекте Deferred.

Пример 1. Невозможно поймать IOException() с помощью внешнего try-catch.
Мы не можем обернуть весь CoroutineScope с помощью try-catch, приложение все равно выйдет из строя.

Пример 2: ловит IOException() с CoroutineExceptionHandler.
Если исключение отличается от CancellationException(), например, IOException(), оно будет распространено на CoroutineExceptionHandler.

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

Пример 4. Используйте invokeOnCompletion, чтобы получить всю информацию об исключениях.
CancellationException() не будет распространяться на CoroutineExceptionHandler.. Если мы хотим распечатать некоторую информацию после возникновения исключения, мы можем использовать invokeOnCompletion для его достижения.

(3) Job
Управляет временем жизни сопрограммы. У задания есть следующие состояния:
Мы можем просто использовать Job.isActive, чтобы узнать текущее состояние задания.

А вот поток смены состояний:

  1. Задание активно, пока работает сопрограмма.
  2. Если задание не выполнено, за исключением исключения, оно отменяется. Задание можно отменить в любой момент с помощью функции отмена, которая заставляет его немедленно перейти в состояние отмены.
  3. Задание отменяется, когда завершается выполнение своей работы.
  4. Родительское задание ожидает в состоянии завершения или отмены для завершения всех своих дочерних заданий перед завершением. Обратите внимание, что состояние завершение - это чисто внутреннее по отношению к работе. Для внешнего наблюдателя задание завершение все еще активно, в то время как внутренне оно ожидает своих дочерних элементов.

(3.1) Иерархии родитель-потомок
После определения состояний нам нужно знать, как работают иерархии родитель-потомок. Допустим, мы пишем такой код:

val parentJob = Job()
val childJob1 = CoroutineScope(parentJob).launch {
    val childJob2 = launch { ... }
    val childJob3 = launch { ... }
}

Тогда иерархия родитель-потомок будет такой:

Мы можем изменить родительскую задачу при запуске следующим образом:

val parentJob1 = Job()
val parentJob2 = Job()
val childJob1 = CoroutineScope(parentJob1).launch {
    val childJob2 = launch { ... }
    val childJob3 = launch(parentJob2) { ... }
}

Тогда иерархии родитель-потомок будут:

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

  • Отмена родителя ведет к немедленной отмене всех его детей.
  • Отказ или отмена дочернего элемента за исключением CancellationException немедленно отменяет его родительский и другие дочерние элементы. Но если исключение - CancellationException, другие задания, не относящиеся к заданию отмены, не будут затронуты.

Если мы выбросим CancellationException, будет отменено только задание под childJob1.

Если мы добавим IOException в одно из дочерних заданий, все соответствующие задания будут отменены:

  • CancelChildren (): родитель может аннулировать своих собственных потомков (включая всех своих потомков рекурсивно), не отменяя себя. Обратите внимание, что если задание отменено, его нельзя будет использовать в качестве родительского для повторного запуска сопрограммы.

Если мы используем Job.cancel(), родительское задание начнет отменяться (Cancelling). И после того, как все дочерние задания будут отменены, статус родительского задания станет cancelled.

Если мы используем вместо этого Job.cancelChildren(), родительское задание по-прежнему будет Active. И мы все еще можем использовать его для запуска других сопрограмм.

(3.2) SupervisorJob против. Работа

Потомки должности супервизора могут потерпеть неудачу независимо друг от друга.

Как мы уже говорили ранее, если мы используем простое Job() в качестве родительского задания, все дочерние элементы будут отменены, если одно из дочерних заданий выйдет из строя:

Если мы используем SupervisorJob() в качестве родительского задания, отказ одного дочернего задания не повлияет на другие дочерние задания:

4. Конструктор сопрограмм

(1) запуск:
запускает новую сопрограмму без блокировки текущего потока и возвращает ссылку на сопрограмму как задание.

(2) async and await:
Конструктор сопрограмм async определен как расширение на CoroutineScope. Он создает сопрограмму и возвращает ее будущий результат как реализацию Deferred, который является неблокируемым отменяемым будущим - это Задание с результатом.

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

Следующий код демонстрирует последовательный вызов двух приостанавливающих функций. Мы выполняем трудоемкую задачу, которая будет стоить 1 секунду как в fetchDataFromServerOne(), так и в fetchDataFromServerTwo(). А затем вызовите их в построителе launch. Мы найдем, что окончательная временная стоимость будет равна сумме временных затрат: 2 секунды.

Журнал будет:

2019-12-09 00:00:34.547 D/demo: fetchDataFromServerOne()
2019-12-09 00:00:35.553 D/demo: fetchDataFromServerTwo()
2019-12-09 00:00:36.555 D/demo: The sum is 3
2019-12-09 00:00:36.555 D/demo: Completed in 2008 ms

Стоимость времени - это сумма времени задержки двух функций приостановки. Он будет приостановлен до завершения fetchDataFromServerOne(), затем выполнится fetchDataFromServerTwo().

Что, если мы хотим запускать обе функции одновременно, чтобы сократить временные затраты? Async приходит на помощь! Async очень похож на launch. Он запускает другие сопрограммы, которые работают одновременно со всеми другими сопрограммами, и возвращает Deferred, который является Job с возвращаемым значением.

public interface Deferred<out T> : Job {
  public suspend fun await(): T
  ...
}

Мы можем вызвать await() для отложенного значения результата. Например:

Журнал будет:

2019-12-08 23:52:01.714 D/demo: fetchDataFromServerOne()
2019-12-08 23:52:01.718 D/demo: fetchDataFromServerTwo()
2019-12-08 23:52:02.722 D/demo: The sum is 3
2019-12-08 23:52:02.722 D/demo: Completed in 1133 ms

5. Тело сопрограммы

Код, выполняемый в CoroutineScope, включая обычную функцию или suspending functions, который приостанавливает выполнение сопрограммы до ее завершения. Подробности мы расскажем в следующем посте.

Это все на сегодня. В следующем посте мы подробнее узнаем о Suspending functions и о том, как его использовать. Надеюсь, этот пост поможет вам узнать больше о Kotlin Coroutines. Если у вас есть какие-либо вопросы или предложения, оставляйте комментарии ниже. Спасибо за чтение, до встречи. 😄

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

  • Официальная документация по Kotlin Coroutines
    Перед использованием рекомендуется прочитать официальную документацию.


  • Официальные кодовые метки Kotlin Coroutines


  • Отличная средняя серия, представляющая Kotlin Coroutines:


  • Руководитель группы библиотек Kotlin рассказывает о блокировке потоков и приостановке сопрограмм.