Разница между нитью и сопрограммой в Котлине
Есть ли какая-либо конкретная языковая реализация в Котлин, которая отличается от других языков?
- Что означает, что сопрограмма похожа на легкую нить?
- В чем разница?
- Выполняются ли параллельные/параллельные транзакции kotlin coroutines?
- Даже в многоядерной системе в любой момент времени работает только одна сопрограмма (правильно?)
Здесь я запускаю 100000 сопрограмм, что происходит за этим кодом?
for(i in 0..100000){
async(CommonPool){
//run long running operations
}
}
Ответы
Ответ 1
Поскольку я использовал сопрограммы только на JVM, я буду говорить о бэкэнде JVM, есть также Kotlin Native и Kotlin JavaScript, но это поддерживает для Kotlin из моего объема.
Итак, давайте начнем с сопоставления сопрограмм Kotlin с другими языковыми сопрограммами. В основном вы должны знать, что это два типа Coroutines: без штабелей и стеков. Kotlin реализует стекированные сопрограммы - это означает, что coroutine не имеет собственного стека, и это ограничивает немного, что может сделать coroutine. Вы можете прочитать хорошее объяснение здесь.
Примеры:
- Stackless: С#, Scala, Kotlin
- Стоки: Квазар, Джавафлоу
Что означает, что сопрограмма похожа на легкую нить?
Это означает, что coroutine в Kotlin не имеет собственного стека, он не отображается на собственный поток, он не требует переключения контекста на процессор.
В чем разница?
Thread - упреждающая многозадачность. (обычно).
Coroutine - совместная многозадачность.
Thread - управляется ОС (обычно).
Coroutine - управляется пользователем.
Действительно ли выполняются параллельные/параллельные сопрограммы kotlin?
Это зависит, вы можете запускать каждую сопрограмму в собственном потоке или вы можете запускать все сопрограммы в одном потоке или в некотором фиксированном пуле потоков.
Подробнее о том, как coroutines выполняет здесь.
Даже в многоядерной системе есть только одна сопрограмма, выполняемая в любой момент времени (правильно?)
Нет, см. предыдущий ответ.
Здесь я запускаю 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).
Ответ 2
Что означает, что сопрограмма похожа на легкую нить?
Coroutine, как поток, представляет последовательность действий, которые выполняются одновременно с другими сопрограммами (потоками).
В чем разница?
Нить напрямую связана с собственным потоком в соответствующей ОС (операционной системе) и потребляет значительное количество ресурсов. В частности, он потребляет много памяти для своего стека. Вот почему вы не можете просто создавать потоки 100k. Вероятно, у вас не хватает памяти. Переключение между потоками связано с диспетчером ядра ОС, и это довольно дорогостоящая операция с точки зрения потребления циклов процессора.
С другой стороны, сопрограмма - это чисто абстракция языка на уровне пользователя. Он не связывает никаких собственных ресурсов и в простейшем случае использует только один относительно небольшой объект в куче JVM. Вот почему легко создавать 100 тыс. Сопрограмм. Переключение между сопрограммами не связано с ядром ОС. Это может быть так же дешево, как вызывать регулярную функцию.
Являются ли kotlin сопрограммы фактически запущенными параллельно/одновременно? Даже в многоядерной системе в любой момент времени работает только одна сопрограмма (правильно?)
Контур может быть запущен или приостановлен. Подстановленный сопрограмм не связан ни с какими конкретными потоками, но работающий сопрограмм работает на каком-то потоке (использование потока - единственный способ выполнить что-либо внутри процесса ОС). Независимо от того, работают ли разные сопрограммы в одном потоке (таким образом, можно использовать только один процессор в многоядерной системе) или в разных потоках (и, следовательно, могут использовать несколько процессоров), это находится исключительно в руках программиста, который использует сопрограммы.
В Котлине диспетчеризация сопрограмм контролируется через контекст coroutine. Вы можете больше узнать об этом в
Руководство по kotlinx.coroutines
Здесь я запускаю 100000 сопрограмм, что происходит за этим кодом?
Предполагая, что вы используете функцию launch
и CommonPool
из проекта kotlinx.coroutines
(который является открытым исходным кодом), вы можете изучить их исходный код здесь:
launch
просто создает новую сопрограмму, а CommonPool
отправляет сопрограммы в ForkJoinPool.commonPool()
, который использует несколько потоков и, таким образом, выполняет на нескольких процессорах в этом примере.
Код, который следует за вызовом launch
в {...}
, называется приостановкой лямбда. Что это такое и как приостановить lambdas и функции, реализованные (скомпилированные), а также стандартные библиотечные функции и классы, такие как startCoroutines
, suspendCoroutine
и CoroutineContext
, объясняются в соответствующем Kotlin coroutines design document.