Часть 1, Часть 2
Эта серия постов подробно посвящена отменам и исключениям в корутинах. Отмена важна тем, что она помогает избежать выполнения большего количества работы, которое в свою очередь может привести к потере памяти и времени автономной работы. Правильная обработка исключений является ключом к отличному пользовательскому опыту. В качестве основы для других 2 частей серии (часть 2: отмена, часть 3: исключения) важно определить некоторые основные понятия, связанные с корутиной, такие как CoroutineScope
, Job
и CoroutineContext
, чтобы мы находились на одной волне.
Если вы предпочитаете видео, посмотрите этот разговор из KotlinConf’19 от Флорины Мунтенеску и от меня:
CoroutineScope
отслеживает любую корутину, созданную с помощью launch
или async
(это функции расширения в CoroutineScope
). Текущая работа (запущенные корутины) может быть отменена вызовом scope.cancel()
в любой момент времени.
Вам необходимо создавать CoroutineScope
всякий раз, когда вы хотите запустить и контролировать жизненный цикл сопрограмм в определенном слое вашего приложения. В некоторых платформах, таких как Android, существуют библиотеки KTX, которые уже предоставляют CoroutineScope
в определенных классах жизненного цикла, например viewModelScope
и lifecycleScope
.
При создании CoroutineScope
принимает CoroutineContext
в качестве параметра для своего конструктора. Вы можете создать новую область и корутину, используя следующий код:
// Job и Dispatcher комбинируются в CoroutineContext, который
// опишу чуть позже
val scope = CoroutineScope(Job() + Dispatchers.Main)val job = scope.launch {
// новая корутина
}
Job
— это управляющий корутиной элемент . Для каждой создаваемой корутины (с помощью launch
или async
) он возвращает экземпляр Job
, который однозначно идентифицирует корутину и управляет ее жизненным циклом. Как показано выше, вы также можете передать Job
в CoroutineScope
, чтобы сохранить возможность управления на время жизненного цикла CoroutineScope
.
CoroutineContext
— это набор элементов, определяющих поведение корутины. Он состоит из:
Job
— управляет жизненным циклом корутины.CoroutineDispatcher
— отправляет работу в соответствующий поток.CoroutineName
— имя корутины, полезно для отладки.CoroutineExceptionHandler
— обрабатывает неотловленные исключения, которые будут рассмотрены в 3 части серии о корутинах.Что такое CoroutineContext
новой корутины? Мы уже знаем, что будет создан новый экземпляр Job
, позволяющий нам контролировать жизненный цикл корутины. Остальные элементы будут унаследованы от CoroutineContext
её родителя (либо другой корутины или CoroutineScope
, где была создана корутина).
Поскольку CoroutineScope
может создавать корутины, а вы можете создавать дополнительные корутины внутри корутины, создается неявная иерархия задач. В следующем фрагменте кода написано, как, помимо создания новой корутины с помощью CoroutineScope
, можно создать дополнительные корутины внутри корутины:
val scope = CoroutineScope(Job() + Dispatchers.Main)val job = scope.launch {
// Родительский элемент новой корутины - CoroutineScope
val result = async {
// New coroutine that has the coroutine started by
// launch as a parent
}.await()
}
Корнем этой иерархии обычно является CoroutineScope
. Мы могли бы представить себе эту иерархию следующим образом:
Job
может проходить через множество состояний: новое, активное, завершение, завершенное, отмена и отмененное. Хотя у нас нет доступа к самим состояниям, мы можем получить доступ к свойствам Job
: isActive
, isCancelled
и isCompleted
.
Если корутина находится в активном состоянии, то происходит либо сбой, либо вызов job.cancel()
переведет Job
в состояние Отмены (isActive = false
, isCancelled = true
). Как только все дети Job
завершат свою работу, корутина перейдет в состояние Отмененное и isCompleted = true
.
В иерархии задач каждая корутина имеет родителя, который может быть либо CoroutineScope
, либо другой корутиной. Однако результирующий родительский CoroutineContext
корутины может отличаться от CoroutineContext
родителя, поскольку он вычисляется на основе этой формулы:
Родительский контекст = значения по умолчанию + унаследованный CoroutineContext
+ аргументы
Где:
Dispatchers.Default
— значение по умолчанию CoroutineDispatcher
и “coroutine” по умолчанию для CoroutineName
.CoroutineContext
— это CoroutineContext
созданного им CoroutineScope
или корутины.Примечание: CoroutineContext
можно комбинировать с помощью оператора +
. Поскольку CoroutineContext
— это набор элементов, будет создан новый CoroutineContext
с элементами в правой части от плюса, переопределяющими те, что находятся слева. Например: (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)
Теперь, когда мы знаем, что такое родительский CoroutineContext
новой корутины, его фактический CoroutineContext
будет:
Новый контекст корутины = родительский CoroutineContext
+ Job()
С помощью CoroutineScope
, показанного на рисунке выше, мы создадим новую корутину:
val job = scope.launch(Dispatchers.IO) {
// новая корутина
}
Что такое родительский CoroutineContext
этой корутины и её фактический CoroutineContext
? Смотрите решение на рисунке ниже!
Результирующий родительский CoroutineContext
имеет Dispatchers.IO
вместо области видимости CoroutineDispatcher
, так как он был переопределен аргументом конструктора корутины. Кроме того, нужно проверить, что Job
в родительском CoroutineContext
является экземпляром области Job
(красный цвет), а новый экземпляр Job
(зеленый цвет) был присвоен CoroutineContext
новой корутины.
После части 3 этой серии станет ясно, что CoroutineScope
может иметь другую реализацию Job
под названием SupervisorJob
в своем CoroutineContext
, которая изменяет то, как CoroutineScope
работает с исключениями. Таким образом, новая корутина, созданная с вышеупомянутой CoroutineScope
, может иметь SupervisorJob
в качестве родительского Job
. Однако, если родителем корутины является другая корутина, то родительское задание всегда будет иметь тип Job
.
Перевод статьи Manuel Vivo: Coroutines: First things first
Комментарии