Skip to main content

coroutines

Для реализации асинхронной логики и выделения работы на отдельные потоки на наших проектах используется kotlinx.coroutines.

Материалы​

Beginner​

  • 📄 KotlinLang Docs - Coroutines guide

    • Coroutines basics
      • Beginner
        • launch, delay, runBlocking, Structured concurrency, coroutineScope, Job, Scope builder
        • создание первой корутины и suspend функции, запуск корутин параллельно, обращение к корутине через объект Job
        • корутины не потеряются и не будет утечек, не потеряются ошибки в корутинах (Structured concurrency)
        • отличия между runBlocking и coroutineScope (Scope builder)
        • обращение к корутине через объект Job (explicit job)
        • пример легковестности корутин по сравнению с потоками
    • Cancellation and timeouts
      • Beginner
        • Job, cancel, cancelAndJoin, isActive, ensureActive, NonCancellable, withTimeout, withTimeoutOrNull, CancellationException, TimeoutCancellationException
        • в корутине нужно проверять, хотят ли её отменить
        • проверка на отмену происходит в каждом suspension point, чтобы проверять чаще используйте: ensureActive. Или проверяйте статус Job с помощью флага isActive (Making computation code cancellable)
        • пример правильной и неправильной отмены корутины (Cancellation is cooperative)
        • работа корутины по time-out (Timeout)
        • работа с ресурсами внутри withTimeout блока, пример проблемы и решения (timeout and resources)
    • Composing suspending functions
    • Coroutine context and dispatchers
      • Beginner
        • CoroutineContext, CoroutineScope, Dispatcher, Unconfined, newSingleThreadContext, Job, join, asContextElement
        • про Dispatcher и как он связаны с потоками (Dispatchers and threads)
        • отладка корутин, используя дебаггер в IDEA или логгирование, CoroutineName (Debugging with IDEA)
        • работа корутин в разных потоках, освобождение создонного потока (Jumping between threads)
        • как получить Job из контекста (Job in the context)
        • дочерние и родительские корутины
        • описание CoroutineScope
        • как использовать несколько контекстов для создания, неассоциативный + (Combining context elements)
        • передача локальных данных потока в корутины или между ними (Thread-local data)
    • Asynchronous Flow
      • Beginner
        • Flow, emit, collect, flowOf, asFlow, operators, cold, hot, builders, cancel
        • что такое Flow, операторы Flow, остановка Flow
        • холодные и горячие Flow
        • Flow builders
      • Intermediate
        • transform, take, numbers, toList, toSet, first, single, reduce, fold, flow, flowOn, buffer, conflate, collectLatest, zip, combine, flatMapConcat, flattenConcat, flatMapMerge, flattenMerge, flatMapLatest, try catch, finally, onCompletion, launchIn
    • Channels
      • Beginner
        • Channel, send, receive, produce, consumeEach, capacity, buffer, tick
        • что такое Channel, типы каналов
        • как происходит закрытие канала (Closing and iteration over channels)
        • паттерн производитель/потребитель (Building channel producer)
        • паттерн конвеер (Pipeline)
        • работа с каналом из нескольких корутин одновременно, справедливая работа канала (Fan-in, Fun-out, Channels are fair)
        • емкость канала
    • Coroutine exceptions handling
      • Beginner
        • cancel, CancellationException, CoroutineExceptionHandler, SupervisionJob, SupervisionScope
        • обработка ошибок в корутинах (Exception propagation)
        • настраиваемый обработчик ошибок (CoroutineExceptionHandler)
        • игнорируемое исключение CancellationException при завершении корутины (Cancellation and exceptions)
        • обработка нескольких исключений от дочерних корутин (Exceptions aggregation)
        • SupervisionJob и SupervisionScope
    • Shared mutable state and concurrency
      • Beginner
        • shared mutable state, Thread-safe
        • проблема изменения общего значения из параллельных корутин
        • решения проблемы: потокобезопасные типы данных, ограничение потоков, mutex (Thread-safe data structures, fine-grained, coarse-grained )
        • пример потраченного времени на переключение между потоками
      • Intermediate
        • что такое mutex (Mutual exclusion)
      • Advanced
        • volatile, Threadsafe, mutex
        • Actors (устаревший подход, actor deprecated с kotlinx.coroutines 1.5.2, рекомендуется использовать Channel напрямую)
    • Debug coroutines using IntelliJ IDEA – tutorial - как дебажить корутины в IDEA
    • Debug Kotlin Flow using IntelliJ IDEA – tutorial - как дебажить flow в IDEA
  • 📄 Корутины в Kotlin (гайд)

    • coroutine, Thread, suspend, runBLocking, launch, join, async, await, Deferred, CoroutineContext, CoroutineDispatсher, Thread safe, Actor, Channel, send, receive
    • Beginner
      • отличия потоков в Java с корутинами в Kotlin
      • два простых примера с пояснениями (runBlocking, launch, suspend, join, async, await, Deferred)
      • CoroutineContext и CoroutineDispatсher - что это и зачем
      • Thread-safe, Actor и Channel
  • 📄 Roman Elizarov - Structured Concurrency

    • Structured Concurrency, coroutineScope
    • Beginner
      • как пришли к Structured Concurrency, какие проблемы решает, какие преимущества дает
      • корутины не глобальные, как потоки. Они связаны со своим скоупом, который представляет собой объект с ограниченным временем жизни. Например элемент UI.
  • 🎦 KotlinConf 2019: Asynchronous Data Streams with Kotlin Flow by Roman Elizarov

    • suspend, Channel, hot, flow, operators, Flowable
    • Beginner
      • 2:15 Введение, как работают suspend функции, проблема runBlocking
      • 4:55 Channel
      • 7:12 горячие каналы, проблемы каналов
      • 9:40 Flow, принцип работы, операторы
      • 13:42 Flow vs List
      • 18:44 Flowable
      • 24:03 Flow
  • 📄 Structured Concurrency in action! (using Kotlin coroutines)

    • Structured Concurrency, cancellation, Job, isActive, CoroutineScope, CoroutineContext
    • Beginner
      • основные концепции Structured Concurrency
      • описание поведения отмены, примеры
      • Параллельная декомпозиция, какие проблемы решает CoroutineScope
      • CoroutineScope и CoroutineContext, в чем разница? (Scope - область действия корутины, Context - элементы жизненного цикла корутины, переменные и константы, с которыми она работает)
      • демонстрация нарушения Structured Concurrency, если корутине передать Context, отличающающийся от контекста родительского CoroutineScope этой корутины
  • 🎦 Александр Нозик. Кое-что о корутинах

    • Beginner
      • Deferred, Structured Cuncurrency, Job, CoroutineScope, GlobalScope, cancel,
      • 22:55 хорошее объяснение, как ведут себя корутины при передаче Deferred
      • 29:32 проблема с потоками, почему они занимают много памяти
      • 52:49 пример проблемы в корутинах, которую решает Structured Concurrency
      • 57:38 Job позволяет работать с результатом корутины, вырубать корутину и еще всякое, но это не сама корутина
      • 1:00:42 CoroutineScope
      • 1:5:28 GlobalScope, это пустышка, нет контекста, нет родителей, не знает о других запусках из GlobalScope, поэтому нарушается Structured Cuncurrency, некуда прокинуть результат и прочее
      • 1:25:20 Если корутина была закрыта с помощью cancel, то закрывает только детей, родителей не трогает
      • 1:26:57 Поведение при возникновении ошибки: если в корутине возникла ошибка, то она закрывает себя, сообщает родителю что случилось, закрывает всех потомков
      • 1:34:10 Нельзя закрывать корутину где угодно, нужно делать в точках расщепления, где проверяется, что она не закрыта
  • 🎦 Александр Нозик. Кое-что о корутинах. Flow, Scope

    • Видео хорошее, рекомендуется к просмотру целиком (x1.5)
    • Beginner
      • CoroutineContext, EmptyCoroutinesContext, Job, GlobalScope launch, newCoroutinesContext, cancel, supervisorScope, async, await, Deferred, Channel, isActive, Flow, cold, hot
      • 8:15 CoroutineContext
      • 12:55 используя EmptyCoroutinesContext вы никак не пользуетесь преимуществами StructuredConcurrency
      • для использования StructuredConcurrency необходим объкет Job, который будет помнить детей и родителя этой корутины
      • 15:29 GlobalScope - пустышка, хранит и использует EmptyCoroutinesContext
      • 19:59 разбор launch, newCoroutinesContext
      • 30:00 способ завершить все корутины - отменить корневой Scope
      • 34:12 нельзя держать какой-нибудь Scope и периодический давать ему задачи
      • 45:26 supervisorScope - когда внутри происходит ошибка, он не обрушивает Scope-родительского
      • 52:31 async похож на Job, отличие в том, что он возвращает Deferred, await() - позволяет использовать результат, когда он будет получен.
      • 55:11 вместо async можно использовать suspend функцию, единственное отличие в том, что async можно отменить, в отличие от suspend функции
      • 57:11 Channel, служит для обмена данными между корутинами, виды каналов
      • 57:49 while(isActive){...} - проверяет, отменена ли данная корутина, и если она отменена, то не входит в новую итерацию цикла. Если использовать while(true){...}, то тот же самый эффект будет в ближайшем suspension point
      • 1:02:41 типы каналов
      • 1:10:42 Flow
        • 1:15:43 холодные - следующий элемент вычисляется не тогда, когда он готов, а когда кто-нибудь его запросил из Flow.
        • 1:29:41 горячий - возвращает элемент сразу же, как сгенерит
    • Intermediate
      • 8:43 CoroutinesContext, отличия от map - строгое типизирование ( если использовать ключ <Е>, то получите объект типа Е ), сумма двух CoroutineContext не ассоциотивна ( CoroutineContext1 + CoroutineContext2 != CoroutineContext2 + CoroutineContext1 т.к если в CoroutineContext1 есть ключи из CoroutineContext2, то при сумме значения по этим ключам перезапишутся из CoroutineContext2 )
  • 📄 Hands-on: Intro to coroutines and channels

  • 🎦 RedMadRobot - Coroutines. Хаотичное изучение. Часть 1

    • Dispatcher, withContext, NonCancellableContext, viewModelScope, suspend, main-thread
    • Beginner
      • 4:02 виды Dispatcher
      • 5:18 что такое withContext и пример с NonCancellableContext
      • 8:10 viewModelScope - готовый Scope, привязанный к жизни компонента
      • 9:38 вызовы suspend функций должны быть безопасны для main-thread
  • 🎦 RedMadRobot - Coroutines. Хаотичное изучение. Часть 2

    • Scope, CoroutineContext, Job, Dispatcher, CoroutineName, CoroutineExceptionHandler, CoroutinesScope, SupervisorScope, Deferred, async, await cancellable
    • Beginner
      • 3:05 CoroutinesScope создает новый Scope, копирует все из Scope-родителя и исполняет переданный ему блок
      • 3:59 supervisorScope не упадет, если упадет ребенок. Если упадет сам Scope, то не упадет Scope-родитель
      • 5:11 Job - фоновая работа, имеет ЖЦ(active, cancelled). Job предоставляет управление корутиной, можем вызвать Job.cancel и тд, не имеет результата
      • 7:06 Deferred это Job, но с результатом, создается myScope.async{...}. Получить значение - myDeferred.await() - вернет занечение или исплючение при ошибке
      • 8:55 как запустить работу во внешнем Scope, что делать, если нам не подходит ViewModelScope, потому что работа должна жить дольше чем ViewModel
      • 15:21 что такое CoroutineContext, что в нем может находиться (Job, CoroutineDispatcher, CoroutineName, CoroutineExceptionHandler)
      • 16:57 разница между CoroutineScope и CoroutineContext
      • 19:32 когда Job отменили, корутина не отменится автоматичеки, разбор кейса
  • 🎦 RedMadRobot - Coroutines. Хаотичное изучение. Часть 3

    • Channel, capacity, close, trySend, Flow, buffer, conflate, SharedFlow, hot, cold, shareIn, whileSubscribed, timeout, Job, lifecycle, repeatOnLifecycle, DESTROYED, flowWithLifecycle
    • Beginner
      • 7:50 Channel - канал для обмена, можно положить и получить, не блокирующий (оперции саспендятся), можно закрыть, разные capacity(RENDEZVOUS, UNLIMITED, CONFLATED, BUFFERED)
      • 3:38 SingleLiveEvent, что это и зачем (события нужно обрабатывать только один раз)
      • 11:30 SingleLiveEvent используя Channel
      • 15:52 как добавить к flow буффер на случай медленного получения
      • 17:25 conflate, сокращение для buffer с параметрами CONFLATED и DROP_OLDEST, т.е хранит одно значение и перезаписыват его
      • 18:07 flowOn переключает контекст выполнения операторов идущих до него, если операторы без своего контекста
      • 20:07 shareIn превращает холодный Flow в горячий SharedFlow
      • 21:52 WhileSubscribed запускает корутину при первом подписчике, остонавливает когда пропадет последний подписчик, можно сохранить кэш при выключении или стереть после timeout
      • 23:12 На что можно заменить Job.cancel()? можно Lifecycle.repeatOnLifecycle
      • 23:39 Lifecycle.repeatOnLifecycle запускает корутину, когда ЖЦ подходит до определенного состояния, когда ЖЦ ниже нужного состояния - отменяет корутину и усылпяет(suspend), когда снова в нужном состоянии - запускает повторно
      • 24:42 особенности repeatOnLifecycle, рекомендуется создавать либо в Activity.onCreate() или Fragment.onViewCreated()
      • 25:22 поведение при DESTROYED, держет в suspend внешнюю корутину, пока не DESTROYED, когда DESTROYED - отпустит
      • 26:20 Flow.flowWithLifecycle это обертка над repeatOnLifecycle, упрощает написание если только 1 продюсер
  • 📄 Guide to UI programming with coroutines

    • UI, dispatcher, context, Dispatchers.Main, Dispatchers.JavaFx, Dispatchers.Swing, UI coroutine, cancel UI coroutine, actor, RendezvousChannel, capacity, ConflatedChannel, Channel.UNLIMITED, UI freeze, Structured concurrency, lifecycle, parent-child hierarchy
  • 📄 Best practices for coroutines

    • Dispatcher, suspend, ViewModel, mutable, Flow, test, TestDispatcher, GlobalScope, cancel, cancellable, ensureActive
    • Intermediate
      • почему не нужно хардкодить Dispatcher
      • suspend функции должны быть безопасны для основного потока, т.е. классы, вызывающие suspend функции не должны беспокоиться о том, какой Dispatcher использовать, эта ответственность лежит на классе, который выполняет эту работу
      • ViewModel должен создавать корутины, а не suspend-функции
      • предоставляйте неизменяемые типы другим классам
      • для классов данных и бизнес-уровня необходимы должны предоставлять suspend функции для одноразовых вызовов и Flow для изменяемых данных
      • используйте StandardTestDispatcher или UnconfinedTestDispatcher в тестах (TestCoroutineDispatcher deprecated с kotlinx.coroutines 1.6)
      • избегайте GlobalScope (это неконтролируемая область, очень усложняет тестирование, нет обзего CoroutineContext)
      • suspend функции должны быть cancellable
  • 🎦 Roman Elizarov — Structured concurrency

  • CodeLab от JetBrains с основами применения механизмов корутин и каналов

Advanced​

Highlights​

Внутреннее устройство​

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

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

Приостановленную сопрограмму можно сохранять и передавать как объект, который хранит её приостановленное состояние и локальные переменные. Типом таких объектов является Continuation, а преобразование кода, описанное здесь, соответствует классическому Continuation-passing style. Следовательно, приостановливаемые функции принимают дополнительный параметр типа Continuation (сохранённое состояние) под капотом.

Более детально о том, как работают сопрограммы, можно узнать в этом проектном документе. Похожие описания async / await в других языках (таких как C# или ECMAScript 2016) актуальны и здесь, хотя особенности их языковых реализаций могут существенно отличаться от сопрограмм Kotlin.

Source (архивная статья).

Пример преобразования кода на этапе компиляции:

Исходник:

dummy() // suspend
println(1)
dummy() // suspend
println(2)

При компиляции преобразуется в:

val $result: Any? = null
when (this.label) {
0 -> {
this.label = 1
$result = dummy(this)
if ($result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
goto 1
}
1 -> {
println(1)
this.label = 2
$result = dummy(this)
if ($result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
goto 2
}
2 -> {
println(2)
return Unit
}
else -> {
throw IllegalStateException("call to 'resume' before 'invoke' with coroutine")
}
}

Более подробно можно прочитать в документе

Также можно прочитать статью или посмотреть видео.

CoroutineScope​

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

Например, у каждой ViewModel есть viewModelScope - это CoroutineScope, который создается при создании ViewModel и отменяется при вызове onCleared.

Рассмотрим небольшую тестовую ViewModel:

class TestViewModel: ViewModel() {
init {
viewModelScope.launch {
delay(10000)
println("work done!")
}
}
}

Если мы зайдем на экран, использующий эту ViewModel, и сразу уйдем (не дожидаясь 10 секунд), то сообщение не будет выведено в лог. А если подождем 10 секунд - будет. Это как раз потому что при вызове onCleared отменяется CoroutineScope, а вместе с ним и все его корутины. А вызов onCleared происходит при окончательном уходе с экрана (прямо перед удалением ViewModel).

Также важно понимать что скоуп может быть вложенным (любой уровень вложенности поддерживается). Для создания вложенного скоупа используется билдер coroutineScope. При отмене родительского скоупа отменяются и все вложенные.

Вложенный скоуп может пригодиться в случаях, когда вам не доступен класс текущего CoroutineScope, но нужно вызвать один из билдеров (launch/async).

suspend fun doSomeParallelWork() {
coroutineScope {
val task1 = async { callFirstRequest() }
val task2 = async { callSecondRequest() }
awaitAll(task1, task2)
}
}

Все корутины принадлежат какому либо скоупу. Скоуп либо создается вручную (вызовом CoroutineScope), либо используется GlobalScope, который не имеет ограничения жизненного цикла - живет все время жизни процесса приложения, поэтому не рекомендуется к использованию без твердой уверенности что это верно.

Использование async​

При запуске асинхронных задач (вызов async) важно учитывать, что возникновение ошибки внутри задачи будет вызывать отмену текущего Scope. Поэтому важно запускать дочерний скоуп используя coroutineScope для выполнения асинхронных задач - тогда ошибка выполнения этих задач не сломает основной CoroutineScope, а будет выброшена как результат coroutineScope и сможет обработаться try-catch.

Например в следующем коде:

suspend fun main() {
coroutineScope {
launch {
try {
listOf<Deferred<Unit>>(
async { TODO() },
async { TODO() }
).awaitAll()
} catch(exc: Throwable) {
println(exc.stackTraceToString())
}
}
}
}

Блок catch не отловит ошибку, произойдет вылет приложения.

Исправленный вариант:

suspend fun main() {
coroutineScope {
launch {
try {
coroutineScope {
listOf<Deferred<Unit>>(
async { TODO() },
async { TODO() }
).awaitAll()
}
} catch(exc: Throwable) {
println(exc.stackTraceToString())
}
}
}
}

coroutineScope это suspend-функция, которая создаёт новый скоуп и идёт дальше только после его завершения и не привязывается к родительскому скоупу. Когда скоуп, созданный coroutineScope, упал, падает сама suspend-функция, а не весь родительский scope.

CoroutineContext​

Корутина, запущенная другой, наследует весь контекст той, внутри которой она запустилась и становится для нее дочерней. Если остановить родителскую корутину, то остановятся и все дочерние корутины. Способы переопределения дефолтного поведения наследования:

  • явно указать DifferentScope.launch
  • передать другой объект Job в качестве контекста корутины

В обоих случаях корутина не привяжется к области, из которой она была запущенна.

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

Dispatcher​

Dispatcher определяет, какой поток или потоки использует корутина для выполнения. Может ограничить выполнение корутины одним потоком, отправить корутину в пулл потоков или никак ее не ограничивать. Основные диспетчеры: Dispatchers.Default (пул потоков для CPU-интенсивных задач), Dispatchers.IO (пул потоков для I/O операций), Dispatchers.Main (главный/UI поток), Dispatchers.Unconfined (без привязки к потоку)

Механика delay​

По началу delay все интерпретируют как Thread.sleep и считают что текущий поток будет остановлен на N миллисекунд. Но это не так . Рассмотрим следующий блок кода:

suspend fun startTimer() {
println("show message at start")
delay(1000)
println("show message after second")
}

При компиляции данный код будет преобразован (если сильно упростить) в нечто похожее на:

fun startTimer() {
println("show message at start")
delayCallback(1000) {
println("show message after second")
}
}

То есть вместо вызова sleep на весь поток, вся работа после suspend-point (delay это suspend функция), будет "завернута в callback", закинута в очередь текущего потока (через Dispatcher корутин) и данный калбек будет выполнен через секунду (когда будет получен из очереди диспатчером).

Вывод - вызов delay не останавливает работу потока.

Более детальное сравнение delay и Thread.sleep можете прочитать в статье

Механика отмены канала​

Для закрытия канала используется метод close() Он посылает специальный токен закрытия и итерация получения элементов из канала остановится, когда токен будет получен Это дает гарантию, что все элементы, отправленные до закрытия, будут получены

Coroutineexceptionhandler​

Дочерние корутины прокидывают свои необработанные ошибки родительской, и так вплоть до корневой. Они не используют ExceptionHandler, установленный в их области действия

Если несколько дочерних корутин выбрасывают исклюения одновременно, то пробрасывается и обрабатывается только первое, а остальные присоединяются к первому как подавленные

Supervision​

Используя SupervisionJob при неудачном завершении дочерней корутины завершение не распростанится на родительскую корутину и других детей Supervision scope распространяет отмену только в отношении дочерних корутин, отменяет всех только если сам завершился с ошибкой. Ожидает завершения всех детей, так же как и coroutineScope. Каждый дочерний элемент обязан самостоятельно обрабатывать свои исключения. Корутины внутри SupervisionScope используют ExceptionHandler установленный в их области действия

Операторы Flow​

  • Intermediate flow operators - основыные операторы похожи на map и filter, отличие от последовательностей в том, что внутри этих операторов можно вызвать suspend функцию
  • Transform operator - применяет функцию в блоке transform для какждого значения
  • Size-limiting operators - отменяют выполнение потока когда лимит достигнут. Отмена происходит с помощью испключения, так что все блоки завершатся корректно
  • Terminal flow operators - toList, toSet, first, reduce, fold (про first и reduce не очень понятно)
  • Flows are sequential - каждый сбор flow по дефолту выполняется последовательно, операторы коллекции выполняются в той же корутине, в которой они были запущены, новые не создаются, каждое переданное значение выполняется всеми промежуточными операторами
  • Flow context - сбор flow всегда проихсодит в контексте вызывающей корутины
  • Wrong emission withContext - нельзя менять контекст внутри flow
  • flowOn operator - приавильный способ изменить контекст для собирания flow
  • Buffering - буферизует выбросы потока через канал указанной емкости и запускает сборщик в отдельной сопрограмме.
  • Conflation - emitter не приостановится из-за медленного коллектора, а удерживает свои элементы, пока коллектор их не запросит, а когда запросит, отправит самый новый элемент из тех, которые накопиились, потом заново начнет копить
  • Processing the latest value - когда flow выдает новое значениеЮ блок действий для старого значения отменяется

SharedFlow и StateFlow​

SharedFlow — горячий поток, который может иметь несколько коллекторов. Значения emit'ятся во все активные коллекции. StateFlow — специализированный SharedFlow, хранящий последнее значение и эмитящий только изменения. Является заменой LiveData в Kotlin Multiplatform.

  • StateFlow and SharedFlow — официальная документация
  • shareIn — превращает холодный Flow в горячий SharedFlow
  • stateIn — превращает холодный Flow в StateFlow
  • SharingStarted.WhileSubscribed — стартует при появлении подписчика, останавливается, когда пропадает последний подписчик
  • SharingStarted.Eagerly — стартует немедленно
  • SharingStarted.Lazily — стартует при первом подписчике, никогда не останавливается

Тестирование корутин​

Начиная с kotlinx.coroutines 1.6, TestCoroutineDispatcher и TestCoroutineScope deprecated. Используйте новый API:

  • runTest — замена runBlockingTest, автоматически продвигает виртуальное время
  • TestScope — тестовый скоуп с контролем виртуального времени
  • StandardTestDispatcher — диспетчер, который ставит задачи в очередь и выполняет их через advanceUntilIdle
  • UnconfinedTestDispatcher — диспетчер, который выполняет задачи немедленно (как Dispatchers.Unconfined)
  • Testing coroutines guide

runInterruptible​

Функция runInterruptible позволяет прерывать блокирующие вызовы (например Thread.sleep, BlockingQueue.take) при отмене корутины. Без неё отмена корутины не прервёт блокирующий вызов на JVM.

suspend fun readBlocking() = withContext(Dispatchers.IO) {
runInterruptible {
Thread.sleep(10000)
}
}