Изучение Котлина
Объем, контекст и работа Kotlin Coroutine стали проще
Изучите базовую экосистему Kotlin Coroutine
Спасибо Ромену Гаю за великолепное фото!
Когда я впервые исследую сопрограмму около 3 лет назад, мы могли просто вызвать launch
напрямую, чтобы запустить неблокирующую сопрограмму.
Теперь это невозможно. Для его запуска нужна область видимости или использование runBlocking
. Но что за прицел?
Официальный документ дает хорошее объяснение, но мне потребовалось 3 дня, чтобы медленно его переварить. Чтобы понять область действия, нужно понимать контекст сопрограммы.
Разобравшись с ней лучше, позвольте мне попытаться упростить обучение, основываясь на моем понимании, приведенном ниже, и некоторых дополнительных выводах, которые я получил за пределами документации.
Основное повторение: что такое Coroutine?
Прежде чем двигаться дальше, давайте вспомним, что такое Coroutine.
- Подобно Thread, что позволяет некоторым задачам работать асинхронно.
- В отличие от Thread, он может работать в том же процессе потока и не блокировать поток (с помощью функции приостановки). Довольно крутая функция. Обратитесь к приведенному ниже документу, чтобы узнать, как это возможно.
Давай поэкспериментируем
runBlocking { println("runBlocking ${Thread.currentThread()}") launch { println("launch ${Thread.currentThread()}") } }
Он напечатает
runBlocking Thread[main @coroutine#1,5,main] launch Thread[main @coroutine#2,5,main]
Чтобы понять вывод на печать Thread, обратитесь к этому StackOverflow. Не беспокойтесь о main thread
и т. Д. На диаграмме. Это будет объяснено позже в разделе диспетчера ниже.
Печать идентификатора сопрограммы в приложении для Android
Примечание. Если вы распечатаете это в приложении Android с использованием Log.d("Track", "....")
, по умолчанию он не будет отображать идентификатор сопрограммы. Вместо этого вы получите следующее.
Track: runBlocking Thread[main,5,main] Track: launch Thread[main,5,main]
Чтобы распечатать идентификатор сопрограммы, вам нужно установить
System.setProperty("kotlinx.coroutines.debug", "on" )
Вы можете установить его в классе Application
class MainApplication: Application() { override fun onCreate() { super.onCreate() System.setProperty("kotlinx.coroutines.debug", "on" ) } }
Тогда вы получите
Track: runBlocking Thread[main @coroutine#1,5,main] Track: launch Thread[main @coroutine#2,5,main]
Что работа"?
Чтобы лучше управлять сопрограммой, предоставляется задание, когда мы launch
(или async
и т. Д.). Задание является частью контекста сопрограммы.
runBlocking { val job = launch { try { println("launch job") delay(1000) println("finish job") } catch (e: CancellationException) { println("cancel job") } } yield() // to start the launch job.cancel() }
В приведенном выше примере с job
можно легко отменить сопрограмму.
launch job cancel job
Задания могут быть иерархическими (родительско-дочерние отношения)
Если launch
запускается в другой сопрограмме (в том же контексте области), job
из launch
станет дочерним job
сопрограммы.
Такие родительско-дочерние отношения вызовут два поведения, описанных ниже.
1. Когда родительская работа отменяется, детская работа также отменяется.
runBlocking { val parentJob = launch { try { println("launch parent job") val childJob = launch { try { println("launch child job") delay(1000) println("finish child job") } catch (e: CancellationException) { println("cancel child job") } } delay(1000) println("finish parent job") } catch (e: CancellationException) { println("cancel parent job") } } delay(500) parentJob.cancel() }
Обратите внимание, что когда родительский job
отменяется, дочерний job
отменяется, как показано в выходных данных ниже.
launch parent job launch child job cancel child job cancel parent job
Однако, если мы отменим job
дочернего элемента, job
родителя продолжится.
val parentJob = launch { try { println("launch parent job") val childJob = launch { try { println("launch child job") delay(1000) println("finish child job") } catch (e: CancellationException) { println("cancel child job") } } delay(500) childJob.cancel() delay(500) println("finish parent job") } catch (e: CancellationException) { println("cancel parent job") } }
Результат, как показано ниже
launch parent job launch child job cancel child job finish parent job
2. Если в работе ребенка возникает ошибка, задание родителя также отменяется.
runBlocking { val parentJob = launch { try { println("launch parent job") val childJob = launch { try { println("launch child job") delay(500) throw Exception() } catch (e: CancellationException) { println("cancel child job") } } delay(1000) println("finish parent job") } catch (e: CancellationException) { println("cancel parent job") } } }
Обратите внимание, когда дочерний job
выходит из строя, родительский job
отменяется, как показано в выходных данных ниже.
launch parent job launch child job cancel parent job
Точно так же, когда родительский job
является ошибочным, дочерний job
отменяется, как показано в выходных данных ниже.
runBlocking { val parentJob = launch { try { println("launch parent job") val childJob = launch { try { println("launch child job") delay(1000) println("finish child job") } catch (e: CancellationException) { println("cancel child job") } } delay(500) throw Exception() println("finish parent job") } catch (e: CancellationException) { println("cancel parent job") } } }
Результат, как показано ниже
launch parent job launch child job cancel child job
Эти два поведения можно суммировать, как показано на диаграмме ниже.
Что такое Диспетчер?
Как и поток, сопрограмма может быть отправлена в различные процессы, чтобы избавить основной поток от необходимости выполнять всю обработку.
В сопрограмме мы используем Dispatchers
в качестве параметра для нашей программы запуска сопрограмм (например, launch
, async
, runBlocking
) в качестве флага, чтобы указать, в какие процессы мы хотим, чтобы она была отправлена. Обратите внимание, что диспетчер также является разновидностью контекста сопрограммы. например launch(Dispatchers.Main)
Есть 5 типов диспетчеров:
Dispatchers.Main
, это основная тема. В отличие от других, иногда нам нужно явно определить его (например, в тестовой среде).Dispatchers.IO
, это для процесса "Сеть и диск". Все, что связано с извлечением или отправкой данных.Dispatchers.Default
, это для любого другого рабочего потока, который не является основным (т.е. фоновым), и он назначается автоматически.Dispatcher.Unconfined
, это специальный диспетчер, который позволяет задаче изменять целевые процессы, когда она приостанавливает и возобновляет выполнение своей задачи.newSingleThreadContex
, позволяют пользователю определять свои собственные процессы. После использования его нужно закрыть вручную.
Когда сопрограмма запускается без предоставленных диспетчеров, она берет диспетчеры у родительского элемента запуска. В противном случае он может определить своего собственного диспетчера, как указано выше.
Безпараметрическая пусковая установка
Если dispatchers
не указан, сопрограмма будет отправлена в те же процессы, что и вызывающая функция.
Как мы видели выше
runBlocking { println("runBlocking pre-yield ${Thread.currentThread()}") launch { println("launch pre-yield ${Thread.currentThread()}") yield() println("launch pos-yield ${Thread.currentThread()}") } yield() println("runBlocking pos-yield ${Thread.currentThread()}") }
И runBlocking
, и launch
будут запущены в основном потоке. Процессы одинаковы независимо от того, до и после yield()
runBlocking pre-yield Thread[main @coroutine#1,5,main] launch pre-yield Thread[main @coroutine#2,5,main] launch pos-yield Thread[main @coroutine#1,5,main] runBlocking pos-yield Thread[main @coroutine#2,5,main]
Параметризация родителя с помощью ребенка без параметров
Если мы предоставим dispatchers
как параметр для runBlocking
, но не launch
, как показано ниже
runBlocking(Dispatchers.IO) { println("runBlocking pre-yield ${Thread.currentThread()}") launch { println("launch pre-yield ${Thread.currentThread()}") yield() println("launch pos-yield ${Thread.currentThread()}") } yield() println("runBlocking pos-yield ${Thread.currentThread()}") }
Вы заметите, что runBlocking
теперь находится в DefaultDispatcher-worker-1
процессе. launch
теперь также находится на DefaultDispatcher
, поскольку он следует за своим родителем. Но это на worker-3
процессах
runBlocking pre-yield Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] launch pre-yield Thread[DefaultDispatcher-worker-3 @coroutine#2,5,main] launch pos-yield Thread[DefaultDispatcher-worker-3 @coroutine#2,5,main] runBlocking pos-yield Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]
Параметризация пусковых установок
Если мы предоставим dispatchers
как параметр для runBlocking
и launch
, как показано ниже
runBlocking(Dispatchers.IO) { println("runBlocking pre-yield ${Thread.currentThread()}") launch(Dispatchers.Main) { println("launch pre-yield ${Thread.currentThread()}") yield() println("launch pos-yield ${Thread.currentThread()}") } yield() println("runBlocking pos-yield ${Thread.currentThread()}") }
Результат такой, как показано ниже. Вы заметите, что launch
больше не отслеживает свои родительские процессы.
runBlocking pre-yield Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] runBlocking pos-yield Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] launch pre-yield Thread[Test Main @coroutine#2,5,main] launch pos-yield Thread[Test Main @coroutine#2,5,main]
Следует отметить, что хотя мы используем
launch(Dispatcher.Main)
, он отличается от значения по умолчаниюmain
. В нашем примере, мы работаем в среде модульного тестирования, этоTest Main
.Dispatcher.Main
необходимо явно определить
private val mainThreadSurrogate = newSingleThreadContext("Test Main") @Before fun setUp() { Dispatchers.setMain(mainThreadSurrogate) } @After fun tearDown() { // reset main dispatcher to the original Main dispatcher Dispatchers.resetMain() mainThreadSurrogate.close() }
А для этого нужна библиотека
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
В приложении для Android прямое использование
Dispatcher.Main
может заблокировать процесс согласно this StackOverflow, если он конкурирует с вызывающей функцией.
Полная таблица комбинаций
Из вышесказанного мы видим, что у нас может быть довольно комбинация диспетчеров, определенных родительскими и дочерними. Вместо того, чтобы просматривать каждый из них, я составил таблицу, в которой все они показаны, как показано ниже.
Из приведенной выше таблицы можно сделать несколько наблюдений.
- Когда запуск из ничего и унаследованный поток
main
отличается от запуска сDispatchers.Main
- При запуске на
Dispatchers.IO
илиDispatchers.Default
используется тот же пул рабочих потоков (пока). - При использовании рабочего потока диспетчера по умолчанию нет гарантии, что он будет использовать один и тот же рабочий поток до и после
yield
. Dispatchers.Unconfined
сначала будет следовать своему родительскому контексту, но впоследствии будет следовать изменениям в зависимости от потока функции приостановки, который он использует.
Вы можете получить код, который перебирает все возможные комбинации выше здесь.
Что такое контекст сопрограммы?
Из вышесказанного мы знаем, что job
и dispatcher
оба являются контекстом сопрограммы. Помимо job
и dispatcher
, существуют другие контексты сопрограмм, такие как CoroutineName, ContinuationInterceptor и CoroutineExceptionHandler.
Контекст сопрограммы - это просто набор элементов (которые также являются контекстами), которые обеспечивают контекст для сопрограммы.
Давайте посмотрим на другие 3 контекста
Имя сопрограммы
Это просто имя для сопрограммы для упрощения отладки.
fun testCoroutineName() { runBlocking { launch(CoroutineName("My-Own-Coroutine")) { println(coroutineContext) } } }
Он выведет что-то, как показано ниже
[CoroutineName(My-Own-Coroutine), CoroutineId(2), "My-Own-Coroutine#2":StandaloneCoroutine{Active}@363ee3a2, BlockingEventLoop@4690b489]
Обратите внимание, что в Android вам необходимо настроить System.setProperty(“kotlinx.coroutines.debug”, “on” )
в приложении, чтобы увидеть имя в соответствии с этим StackOverflow.
Обработчик исключений сопрограммы
Этот контекст позволяет сопрограмме иметь настраиваемую обработку исключений.
fun testCoroutineExceptionHandler() { val handler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler got $exception") } runBlocking { MainScope().launch(handler) { launch { throw IllegalAccessException("Just testing") } }.join() } }
Запуск его приведет к исключению, захваченному с необходимостью try-catch
CoroutineExceptionHandler got java.lang.IllegalAccessException: Just testing
Примечание. Я попытался установить
runBlocking
, но не смог зафиксировать ошибку. Об этом сообщается на StackOverflow.
Продолжение перехватчика
Перехватчик продолжения позволяет определить, как должны продолжаться сопрограммы.
В приведенном ниже коде я просто распечатываю его всякий раз, когда выполняется перехват.
fun testContinuationInterceptor() { val interception = object: ContinuationInterceptor { override val key: CoroutineContext.Key<*> get() = ContinuationInterceptor override fun <T> interceptContinuation( continuation: Continuation<T>): Continuation<T> { println(" ## Interception Setup for ${continuation.context[Job]} ##") return Continuation(continuation.context) { println(" ~~ Interception for {continuation.context[Job]} ~~") continuation.resumeWith(it) } } } runBlocking(CoroutineName("runBlocker") + interception) { println("Started runBlocking") launch(CoroutineName("launcher")) { println("Started launch") delay(10) println("End launch") } delay(10) println("End runBlocking") } }
В результате для каждого delay(10)
будет напечатано ~~Intercdeption
, как показано ниже.
## Interception Setup for "runBlocker#1":BlockingCoroutine{Active}@6a01e23 ## ~~ Interception for "runBlocker#1":BlockingCoroutine{Active}@6a01e23 ~~ Started runBlocking ## Interception Setup for "launcher#2":StandaloneCoroutine{Active}@14d3bc22 ## ~~ Interception for "launcher#2":StandaloneCoroutine{Active}@14d3bc22 ~~ Started launch ~~ Interception for "launcher#2":StandaloneCoroutine{Active}@14d3bc22 ~~ End launch ~~ Interception for "runBlocker#1":BlockingCoroutine{Active}@6a01e23 ~~ End runBlocking
Объединение контекста
Если вы внимательно посмотрите на код выше, вы заметите эту строку
runBlocking(CoroutineName("runBlocker") + interception)
И interception
, и CoroutineName("runBlocker")
являются контекстом. Используя +
, мы объединяем их в новый Context
, который хранит другой как Element
из Context
. Вот как группируются контексты сопрограмм.
Обратите внимание, что оператор
+
имеет некоторую защиту от повторяющихся комбинаций, например. приведенное ниже приведет к ошибке при компиляции.
runBlocking(Dispatchers.IO + Dispatchers.Main){ }
Однако оператор
+
не выдает ошибку во всех случаях. Например. для нижеприведенного он не выдаст ошибку, а будет использовать последнее указанное имя
runBlocking(CoroutineName("Drop Name") + CoroutineName("Taken Name")){ }
Переключение контекста
Иногда нам нужно, чтобы наша сопрограмма переключалась между контекстом, находясь в той же сопрограмме. Мы можем сделать это с помощью withContext
.
fun testSwitchContext() { newSingleThreadContext("Ctx1").use { ctx1 -> newSingleThreadContext("Ctx2").use { ctx2 -> runBlocking(ctx1) { println("Started in ctx1 $coroutineContext") withContext(ctx2) { println("Working in ctx2 $coroutineContext") } println("Back to ctx1 $coroutineContext") } } } }
Используя это, мы получим следующий результат
Started in ctx1 [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@7ff11457, ThreadPoolDispatcher[1, Ctx1]] Working in ctx2 [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@1aefb6f2, ThreadPoolDispatcher[1, Ctx2]] Back to ctx1 [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@7ff11457, ThreadPoolDispatcher[1, Ctx1]]
Вы заметите, что все они по-прежнему CoroutineId(1)
, но переключаются между Ctx1
и Ctx2
.
Что такое Coroutine Scope?
Наконец, что такое Coroutine Scope? Ниже мои 3 цента
- С точки зрения Coroutine Context может быть набором элементов контекста, чтобы лучше управлять ими, он обернут вокруг Coroutine Scope.
- Помимо просто обертывания контекста, Coroutine Scope также отслеживает все его дочерние области. например при выполнении
launch
в другой области автоматически создается дочерняя область. - Он предоставляет все утилиты для
launch
,async
, а такжеcancel
и т. Д. Другими словами, это основной интерфейс разработчика для управления сопрограммами.
Эта статья описывает это более подробно.
Если вы обратили внимание на большую часть приведенного выше примера кода, мне придется использовать runBlocking
, прежде чем я смогу делать какие-либо launch
. Это потому, что launch
может быть запущен только из CoroutineScope.
Какие у нас есть прицелы?
- GlobalScope - если вы просто хотите иметь простой
launch
, не беспокоясь об управлении им, мы можем использоватьGlobalScope.launch { }
. Однако это не идеально, как гласит эта статья. - MainScope - запускает сопрограмму в основном потоке (потоке пользовательского интерфейса) по умолчанию при использовании
MainScope().launch { }
. - CoroutineScope - это позволяет вам определять настраиваемую область, предоставляя свой собственный контекст, например
CoroutineScope(Dispatchers.IO).launch { }
. - LifecycleScope - это связанная с Android область сопрограммы, которая связывает жизненный цикл Android области (то есть способность автоматически завершаться по окончании жизненного цикла Android Activity.
Управление родительско-дочерней областью
По умолчанию, когда мы выполняем launch
в области сопрограммы, у нас есть родительская и дочерняя области, где отмена задания и триггер ошибки будут влиять друг на друга.
MainScope().launch { // this is a parent scope launch { // this is a child scope } }
Однако, если мы хотим разъединить эту связь, мы можем сделать это, создав другой объект Scope в пределах области сопрограммы.
MainScope().launch { // this is a parent scope GlobalScope.launch { // this is a not a child scope of the parent } }
Если launch
выполняется вне области сопрограммы, мы также можем установить отношения родительско-дочерней области, назначив родительское задание определению дочерней области, как показано ниже.
val parentJob = MainScope().launch { // this is a parent scope } CoroutineScope(parentJob).launch { // this is a child scope }
Надеюсь, что приведенное выше дает достаточно простой обзор базовой экосистемы сопрограмм, достаточный для изучения более подробных статей. Ваше здоровье.