Разница между потоком и сопрограммой в Котлине

Есть ли какая-то конкретная языковая реализация в Kotlin, которая отличает ее от реализации сопрограмм на других языках?

  • Что означает, что сопрограмма похожа на легкий поток?
  • В чем разница?
  • На самом ли деле корутины kotlin работают параллельно / одновременно?
  • Даже в многоядерной системе в любой момент времени работает только одна сопрограмма (правильно?)

Здесь я запускаю 100000 сопрограмм, что происходит за этим кодом?

for(i in 0..100000){
   async(CommonPool){
    //run long running operations
  }
}

person Jemo Mgebrishvili    schedule 25.03.2017    source источник
comment
soundcloud.com/user-38099918/coroutines-with-roman-elizarov поговорим о сопрограммах в Котлине   -  person Jemo Mgebrishvili    schedule 18.04.2017


Ответы (2)


Поскольку я использовал сопрограммы только на JVM, я расскажу о бэкэнде JVM, есть также Kotlin Native и Kotlin JavaScript, но эти бэкенды для Kotlin выходят за рамки моей компетенции.

Итак, давайте начнем со сравнения сопрограмм Kotlin с сопрограммами других языков. По сути, вы должны знать, что есть два типа сопрограмм: без стека и со стеками. Kotlin реализует сопрограммы без стека - это означает, что сопрограмма не имеет собственного стека, и это немного ограничивает возможности сопрограммы. Вы можете прочитать хорошее объяснение здесь.

Примеры:

  • Без стека: C #, Scala, Kotlin
  • Полный: Quasar, Javaflow

Что означает, что сопрограмма похожа на легкий поток?

Это означает, что у сопрограммы в Kotlin нет собственного стека, она не отображается на нативный поток и не требует переключения контекста на процессоре.

В чем разница?

Thread - приоритетная многозадачность. (обычно). Coroutine - совместная многозадачность.

Тема - управляется ОС (обычно). Coroutine - управляется пользователем.

Действительно ли сопрограммы котлина работают параллельно / одновременно?

Это зависит от того, вы можете запускать каждую сопрограмму в ее собственном потоке, или вы можете запускать все сопрограммы в одном потоке или в каком-то фиксированном пуле потоков.

Подробнее о выполнении сопрограмм здесь.

Даже в многоядерной системе в любой момент времени работает только одна сопрограмма (правильно?)

Нет, см. Предыдущий ответ.

Здесь я запускаю 100000 сопрограмм, что происходит за этим кодом?

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

fun main(args: Array<String>) {
    for (i in 0..100000) {
        async(CommonPool) {
            delay(1000)
        }
    }
}

Этот код выполняется мгновенно.

Потому что нам нужно дождаться результатов от async вызова.

Итак, давайте исправим это:

fun main(args: Array<String>) = runBlocking {
    for (i in 0..100000) {
        val job = async(CommonPool) {
            delay(1)
            println(i)
        }

        job.join()
    }
}

Когда вы запустите эту программу, kotlin создаст 2 * 100000 экземпляров Continuation, что займет несколько десятков МБ ОЗУ, а в консоли вы увидите числа от 1 до 100000.

Итак, давайте перепишем этот код следующим образом:

fun main(args: Array<String>) = runBlocking {

    val job = async(CommonPool) {
        for (i in 0..100000) {
            delay(1)
            println(i)
        }
    }

    job.join()
}

Чего мы добиваемся сейчас? Теперь мы создаем только 100001 экземпляр Continuation, и это намного лучше.

Каждое созданное продолжение будет отправлено и выполнено в CommonPool (который является статическим экземпляром ForkJoinPool).

person Ruslan    schedule 25.03.2017
comment
Отличный ответ, но я бы предложил сделать одно важное исправление. Сопрограммы в Kotlin раньше не имели стека в первоначальном предварительном предварительном выпуске, но фактически были выпущены в Kotlin 1.1 с поддержкой приостановки на любой глубине стека, как, например, в Quasar. Для тех, кто знаком с Quasar, довольно легко увидеть однозначное соответствие между модификатором Quasar throws SuspendExecution и Kotlin suspend. Детали реализации, конечно, совершенно разные, но пользовательский опыт очень похож. - person Roman Elizarov; 26.03.2017
comment
Вы также можете ознакомиться с подробностями о фактической реализации сопрограмм Kotlin в соответствующем проектный документ. - person Roman Elizarov; 26.03.2017
comment
Получается, что сопрограммы технически лишены стека, но в то же время позволяют приостанавливать работу на любом уровне по глубине? Не стесняйтесь редактировать мой ответ и исправлять его. Также спасибо за вашу работу над проектом kotlin coroutines! - person Ruslan; 26.03.2017
comment
Честно говоря, я не знаю, что означает термин «стековая сопрограмма». Я не видел никакого формального / технического определения этого термина, и я видел, как разные люди использовали его совершенно противоположным образом. Я бы вообще отказался от термина «стековая сопрограмма». Что я могу сказать наверняка и что легко проверить, так это то, что сопрограммы Kotlin намного ближе к Quasar и очень сильно отличаются от C #. Помещение корутинов Kotlin в ту же корзину, что и C # async, кажется неправильным, независимо от вашего конкретного определения слова stackful coroutine. - person Roman Elizarov; 27.03.2017
comment
Я бы классифицировал сопрограммы на разных языках следующим образом: C #, JS и т. Д. Имеют сопрограммы на основе будущих / обещаний. Любое асинхронное вычисление на этих языках должно возвращать какой-то объект, похожий на будущее. Было бы нечестно называть их бесстековыми. Вы можете выражать асинхронные вычисления любой глубины, это просто синтаксически и с точки зрения реализации неэффективно с ними. Kotlin, Quasar и т. Д. Имеют сопрограммы на основе приостановки / продолжения. Они строго более мощные, потому что их можно использовать с объектами будущего или без них, используя только функции приостановки. - person Roman Elizarov; 27.03.2017
comment
Ok. Вот хорошая статья, которая дает представление о сопрограммах и дает более или менее точное определение стековой сопрограммы: inf.puc-rio.br/~roberto/docs/MCC15-04.pdf Это означает, что Kotlin реализует стековые сопрограммы. - person Roman Elizarov; 05.04.2017
comment
Обновленная ссылка на проектный документ сопрограмм Kotlin: github.com/Kotlin/KEEP /blob/master/proposals/coroutines.md - person ASP; 22.04.2020
comment
Coroutine - это не стек, а гибрид. см .: stackoverflow.com/questions/67483210/ - person c-an; 11.05.2021

Что означает, что сопрограмма похожа на легкий поток?

Сопрограмма, как и поток, представляет собой последовательность действий, которые выполняются одновременно с другими сопрограммами (потоками).

В чем разница?

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

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

Действительно ли сопрограммы kotlin работают параллельно / одновременно? Даже в многоядерной системе в любой момент времени работает только одна сопрограмма (правильно?)

Сопрограмма может быть запущена или приостановлена. Приостановленная сопрограмма не связана с каким-либо конкретным потоком, но работающая сопрограмма выполняется в каком-то потоке (использование потока - единственный способ выполнить что-либо внутри процесса ОС). Независимо от того, работают ли все разные сопрограммы в одном потоке (таким образом, a может использовать только один ЦП в многоядерной системе) или в разных потоках (и, следовательно, может использовать несколько ЦП), исключительно в руках программиста, использующего сопрограммы.

В Kotlin отправка сопрограмм контролируется через контекст сопрограмм. Вы можете узнать больше об этом в Руководство по kotlinx.coroutines

Здесь я запускаю 100000 сопрограмм, что происходит за этим кодом?

Предполагая, что вы используете launch функцию и CommonPool контекст из проекта kotlinx.coroutines (который является открытым исходным кодом), вы можете изучить их исходный код здесь:

launch просто создает новую сопрограмму, а CommonPool отправляет сопрограммы ForkJoinPool.commonPool(), которая использует несколько потоков и, таким образом, выполняется на нескольких процессорах в этом примере.

Код, следующий за вызовом launch в {...}, называется приостанавливающей лямбдой. Что это такое и как приостанавливаются (компилируются) лямбды и функции, а также стандартные библиотечные функции и классы, такие как startCoroutines, suspendCoroutine и CoroutineContext, объясняется в соответствующем Проектный документ Kotlin coroutines.

person Roman Elizarov    schedule 05.04.2017
comment
Итак, грубо говоря, означает ли это, что запуск программы похож на добавление задания в очередь потоков, где очередь потоков контролируется пользователем? - person Leo; 15.04.2018
comment
да. Это может быть очередь для одного потока или очередь для пула потоков. Вы можете рассматривать сопрограммы как примитив более высокого уровня, который позволяет вам избежать ручной (повторной) отправки продолжений вашей бизнес-логики в очередь. - person Roman Elizarov; 15.04.2018
comment
так разве это не означает, что когда мы запускаем несколько сопрограмм параллельно, это не настоящий параллелизм, если количество сопрограмм намного больше, чем количество потоков в очереди? Если это так, то это действительно похоже на Executor в Java, есть ли между ними какая-то связь? - person Leo; 15.04.2018
comment
Это не отличается от потоков. Если количество потоков больше, чем количество физических ядер, это не настоящий параллелизм. Разница в том, что потоки планируются на ядрах заранее, тогда как сопрограммы планируются на потоках совместно - person Roman Elizarov; 15.04.2018