Боль: Корутины в Kotlin

 Публичный пост

Принято считать, что Kotlin - очень простой язык. Но в Котлине есть корутины.

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

Я перечитал официальную документацию, покиберсталкал Романа Елизарова в твиттере и на медиуме, полез на душный Хабр. Примеров мало. Очень мало.

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

В общем в этом треде поделиться реальным опытом, чтобы натренировать свое подсознание. Чтобы появилась некая интуиция в корутинах. Какие примеры? Какие угодно, которые вы понимаете и считаете интересными

Скину несколько примеров:

  1. Если мы идем по блоку suspend-функций и упираемся в долгий блокирующий вызов, то правильно его оборачивать в withContext.

Пусть у нас есть функция:


fun findBigPrime() : BigInteger =
    BigInteger.probablePrime(4096, Random())

Правильно будет вызвать ее так:

suspend fun findBigPrime(): BigInteger =
    withContext(Dispatchers.Default) {
        BigInteger.probablePrime(4096, Random())
    }

Этот пример также иллюстрирует необходимость разделять вычисления, блокирующие IO от вычислений, нагружающих процессор. В корутинах это соответственно делается за счет использования диспетчеров Dispatchers.Default и Dispatchers.IO

  1. coroutineScope vs withContext

coroutineScope == withContext(this.coroutineContext)

  1. runBlocking vs coroutineScope

runBlocking - запускает корутину и блокирует вызывающий тред, пока корутина не закончится. runBlocking - не suspend-функция.

coroutineScope - suspend-функция. Если вызывающая корутина суспендится, то coroutineScope точно также засуспендится, что в свою очередь освободит вызывающий тред.

У меня получилось сделать пример где явно видна разница между coroutineScope и runBlocking, но он достаточно корявый

suspend fun main() {
    val elapsedTime = measureTimeMillis {
        withContext(newSingleThreadContext("single")) {
            launch { delay(1000); println("child 0 is finished")}
            coroutineScope {
                launch { delay(1000); println("child 1 is finihed") }
                launch { delay(1000); println("child 2 is finished") }
            }
        }
    }

    println("Main completed in $elapsedTime")
}

Выполняется у меня за 1200 мс.

Тогда как:


 suspend fun main() {
    val elapsedTime = measureTimeMillis {
        withContext(newSingleThreadContext("single")) {
            launch { delay(1000); println("child 0 is finished")}
            runBlocking {
                launch { delay(1000); println("child 1 is finihed") }
                launch { delay(1000); println("child 2 is finished") }
            }
        }
    }

    println("Main completed in $elapsedTime")
}

Отрабатывает за 2200 мс.

  1. GlobalScope in runBlocking

Если запустить GlobalScope внутри runBlocking, то runBlocking не будет дожидаться исполнения GlobalScope.launch. GlobalScope создает "глобальные корутины", которые не являются дочерними от runBlocking

Такой код ничего не выведет:

fun main() {
    runBlocking {
        for (i in 1..2) {
            GlobalScope.launch {
                Thread.sleep(1000)
                println("work $i done")
            }
        }
    }
}

  1. Хороший разбор того, что такое suspend-функция

  2. Логику работы с корутинами можно помещать в extension-функции CoroutineScope. Хотя я, например, когда надо, когда не надо. И все же действует контракт - такая функция запускает параллельно работу в фоне, и немедленно возвращается.

fun CoroutineScope.launchFoo() = launch {
    delay(1000)
    println("foo")
}

Поэтому ни в коем случае не надо писать suspend extension-функции:

suspend fun CoroutineScope.foo() = ???

Связанные посты
12 комментариев 👇

За пост – спасибо. Сам испытываю похожие затруднения: непонятно, мало примеров, не хватает интуиции.

Как справиться?

  • Начать использовать корутины. Для бекенда могу порекомендовать написать небольшой сервис на Ktor: там корутины везде. Т.е. из любого места доступен контекст корутин и можно начать хотя бы с оборачивания блокирующих вызовов (база, сеть) в suspend-функции. А уже потом разобраться с тонкостями контекста и скоупа, launch, async, coroutineScope. Рано или поздно выработается интуиция.
  • Не стеснятся спрашивать. Kotlin удивительный язык ещё и потому, что это, пожалуй, единственная девелоперская технология (не считая 1C), у которой русскоговорящее комньюнити больше и активнее англоязычного. Kotlin широко представлен в [большей частью русскоязычном] Телеграме: https://kotlinby.github.io/kotlin-telegram. В этих чатах сидит большое число разработчиков языка и просто "топчиков", готовых помочь. Ну и конечно всегда есть официальный Slack, Stack Overflow и другие ресурсы.
  Развернуть 1 комментарий

А уже потом разобраться с тонкостями контекста и скоупа, launch, async, coroutineScope. Рано или поздно выработается интуиция.

Вот я где-то здесь и пока непонятно как этот квантовый скачок совершить, от простых оберток в suspend и простых launch, или там простого pipeline на channel'ах до понимания сути. Ок, посмотрю Ktor

  Развернуть 1 комментарий
Роман Останин , Android разработчик 15 мая 2020

у меня был опыт с корутинами год назад примерно

  1. мне не хватало функционала, который может дать RxJava
  2. появилось куча кода в проекте помеченного Experimental предупреждениями
  3. написал код на корутинах, а через полгода все изменилось и оказалось что надо писать уже по-другому и часть кода переписывать

так что выпилил я это все из проекта и стало спокойнее и проще. Сейчас может что-то за год поменялось, я не следил

  Развернуть 1 комментарий

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

  Развернуть 1 комментарий

@StepanPotapov, да мне не хватало еще всяких штук типа BroadcastChannel, ConflatedBroadcastChannel - и они до сих пор экспериментальные. Одно время вообще велось обсуждение - что каналы уберут и будут заменять на Flow. Но сейчас посмотрел - они никуда не делись

  Развернуть 1 комментарий

@n0r0eq1, и не денутся. Речь, скорее, шла о том, чтобы добавить Flow, потому как они с Channels разные.

  Развернуть 1 комментарий

А для каких задач не хватает Rx например? Я не очень давно на нем, но пока что складывается впечатление что на rx и rx-extensions можно сделать вообще всё

  Развернуть 1 комментарий

@zalivka, вы меня не поняли. Я писал, что корутины не являлись полноценной заменой RxJava на тот момент. В RxJava как раз все хватает

  Развернуть 1 комментарий

@n0r0eq1, а сорян

  Развернуть 1 комментарий

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

  Развернуть 1 комментарий

@ganqqwerty, просто взгляните на какое-нибудь видео от Елизарова про корутины

  Развернуть 1 комментарий
Иван Лопатин , Scala-разработчик 15 мая 2020

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

  Развернуть 1 комментарий

😎

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

Что вообще здесь происходит?


Войти  или  Вступить в Клуб