Різниця між ниткою та спільною програмою в Котліні


77

Чи існує якась конкретна мовна реалізація в Котліні, яка відрізняє її від інших мовних програм?

  • Що означає, що співучасть подібна легкій нитці?
  • Яка різниця?
  • Чи насправді програми kotlin працюють паралельно / одночасно?
  • Навіть у багатоядерній системі в будь-який момент часу працює лише одна програма (чи правильно?)

Ось я починаю 100000 підпрограм, що відбувається за цим кодом?

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

Відповіді:


68

Оскільки я використовував підпрограми лише на JVM, я буду говорити про бекенд JVM, є також Kotlin Native та Kotlin JavaScript, але ці серверні програми для Kotlin виходять за межі мого обсягу.

Отже, почнемо з порівняння котлінських програм Котліна та інших мов. По суті, ви повинні знати, що існує два типи корутинів: безстаровий і штабельний. Kotlin реалізує безпроблемні програми - це означає, що програма не має власного стеку, і це трохи обмежує можливості, які може зробити програма. Ви можете прочитати хороше пояснення тут .

Приклади:

  • Бездоганні: C #, Scala, Kotlin
  • Складені: Quasar, Javaflow

Що це означає, що програма є схожою на легку нитку?

Це означає, що програма в Kotlin не має власного стека, вона не відображається у власному потоці, не вимагає перемикання контексту на процесорі.

Яка різниця?

Потік - попереджувально багатозадачність. ( зазвичай ). Coroutine - спільна багатозадачність.

Потік - управляється ОС (зазвичай). Coroutine - керується користувачем.

Чи насправді програми Kotlin працюють паралельно / одночасно?

Це залежить, ви можете запустити кожну підпрограму у своєму потоці, або ви можете запустити всі підпрограми в одному потоці або якомусь фіксованому пулі потоків.

Детальніше про те, як виконуються програми тут .

Навіть у багатоядерній системі в будь-який момент часу працює лише одна програма (чи правильно?)

Ні, див. Попередню відповідь.

Ось я починаю 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).


15
Чудова відповідь, але я б запропонував зробити одну важливу корекцію. Спільні програми в Котліні раніше були бездоганними під час попереднього попереднього перегляду, але насправді були випущені в Котліні 1.1 з підтримкою підвіски на будь-якій глибині стека, як, наприклад, у Квазарі. Для тих, хто знайомий з Quasar, досить легко побачити відповідність 1: 1 між модифікатором Quasar throws SuspendExecutionта suspendмодифікатором Котліна. Звичайно, деталі реалізації досить різні, але досвід користувача досить схожий.
Роман Єлізаров

5
Ви також можете ознайомитись із деталями фактичного впровадження програм Котлін у відповідному проектному документі .
Роман Єлізаров

4
Чесно кажучи, я не знаю, що означає термін "складна програма". Я не бачив жодного офіційного / технічного визначення цього терміна, і я бачив, як різні люди вживають його абсолютно суперечливо. Я б взагалі не використовував термін "складна програма". Що я можу сказати точно, і що легко перевірити, так це те, що програми Котліна набагато ближчі до Квазара і дуже схожі на С #. Поміщення котулінів Котліна в ту саму корзину, що і C # async, не здається правильним, незалежно від вашого конкретного визначення слова "складна програма".
Роман Єлізаров

9
Я класифікував би програми різними мовами таким чином: C #, JS тощо мають такі програми, що базуються на майбутньому / обіцянках . Будь-яке асинхронне обчислення на цих мовах повинно повертати якийсь об’єкт, подібний до майбутнього. Не справді називати їх бездоганними. Ви можете виражати асинхронні обчислення будь-якої глибини, вони просто синтаксично та реалізовуються неефективно з ними. Kotlin, Quasar та ін мають спільні програми на основі суспензії / продовження . Вони суворо потужніші, оскільки їх можна використовувати з об’єктами, подібними до майбутнього, або без них, використовуючи лише функції призупинення.
Роман Єлізаров

7
Гаразд. Ось хороша стаття, яка дає основні відомості про програми та дає більш-менш точне визначення поняття "складна програма": inf.puc-rio.br/~roberto/docs/MCC15-04.pdf Це означає, що Котлін впроваджує стекові програми .
Роман Єлізаров

70

Що означає, що співучасть подібна легкій нитці?

Спільна програма, як і нитка, являє собою послідовність дій, що виконуються одночасно з іншими підпрограмами (потоками).

Яка різниця?

Потік безпосередньо пов’язаний із власним потоком у відповідній ОС (операційна система) і споживає значну кількість ресурсів. Зокрема, він споживає багато пам'яті для свого стека. Ось чому ви не можете просто створити 100k потоків. Ймовірно, у вас закінчиться пам’ять. Переключення між потоками включає диспетчер ядра ОС, і це досить дорога операція з точки зору споживаних циклів процесора.

З іншого боку, корутина - це суто абстракція мови на рівні користувача. Він не пов’язує жодних власних ресурсів і, у найпростішому випадку, використовує лише один відносно невеликий об’єкт у купі JVM. Ось чому легко створити 100 тис. Програм. Переключення між програмами взагалі не передбачає ядра ОС. Це може бути настільки дешево, як виклик звичайної функції.

Чи насправді програми Kotlin працюють паралельно / одночасно? Навіть у багатоядерній системі в будь-який момент часу працює лише одна програма (чи правильно?)

Програма може бути як запущеною, так і призупиненою. Призупинена програма не пов'язана з яким-небудь конкретним потоком, але запущена програма працює на якомусь потоці (використання потоку - єдиний спосіб виконати що-небудь усередині процесу ОС). Чи всі різні програми працюють в одному потоці (а, отже, можуть використовувати лише один процесор у багатоядерній системі), або в різних потоках (і, отже, можуть використовувати кілька центральних процесорів), виключно в руках програміста, який використовує програми.

У Котліні розподіл спільних програм контролюється за допомогою відповідного контексту . Детальніше про це ви можете прочитати у Керівництві по kotlinx.coroutines

Ось я починаю 100000 підпрограм, що відбувається за цим кодом?

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

launchПросто створює нову співпрограми, а CommonPoolдепеші співпрограми , ForkJoinPool.commonPool()який робить використання декількох потоків і , таким чином , виконуються на декількох процесорах в цьому прикладі.

Код, що слідує за launchвикликом у {...}, називається призупиненою лямбда . Що це і як суспендуючі лямбда і функції реалізовані (укладач), а також стандартні бібліотечні функції і класи , як startCoroutines, suspendCoroutineі CoroutineContextпояснюється в відповідному Котлин співпрограми дизайн документа .


3
Отже, грубо кажучи, чи означає це, що запуск курутини подібний до додавання завдання до черги потоків, де чергою потоків керує користувач?
Лев

2
Так. Це може бути черга для одного потоку або черга для пулу потоків. Ви можете розглядати сопрограми як примітив вищого рівня, який дозволяє уникнути вручну (повторного) подання продовжень вашої ділової логіки в чергу.
Роман Єлізаров

отже, чи не означає це, що коли ми паралельно запускаємо декілька програм, це не відповідає дійсності, якщо кількість програм значно більша, ніж кількість потоків у черзі? Якщо це так, то це звучить насправді схоже на Java Executor, чи існує взаємозв'язок між цими двома?
Лев

6
Це не відрізняється від ниток. Якщо кількість потоків більше, ніж кількість фізичного ядра, це не є справжнім паралелізмом. Різниця полягає в тому, що потоки плануються на ядрах попереджувально , тоді як спільні програми плануються на потоках спільно
Роман Єлізаров
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.