Unterschied zwischen Faden und Coroutine in Kotlin

75

Gibt es eine spezifische Sprachimplementierung in Kotlin, die sich von der Implementierung von Coroutinen in einer anderen Sprache unterscheidet?

  • Was bedeutet, dass Coroutine wie ein leichter Faden ist?
  • Was ist der Unterschied?
  • Laufen Kotlin-Coroutinen tatsächlich parallel / gleichzeitig?
  • Selbst in Mehrkernsystemen läuft immer nur eine Coroutine (stimmt das?)

Hier starte ich 100000 Coroutinen. Was passiert hinter diesem Code?

for(i in 0..100000){
   async(CommonPool){
    //run long running operations
  }
}
Jemo Mgebrishvili
quelle
3
soundcloud.com/user-38099918/coroutines-with-roman-elizarov sprechen über Coroutines in Kotlin
Jemo Mgebrishvili

Antworten:

65

Da ich Coroutinen nur für JVM verwendet habe, werde ich über das JVM-Backend sprechen. Es gibt auch Kotlin Native und Kotlin JavaScript, aber diese Backends für Kotlin liegen außerhalb meines Anwendungsbereichs.

Beginnen wir also mit dem Vergleich von Kotlin-Coroutinen mit Coroutinen anderer Sprachen. Grundsätzlich sollten Sie wissen, dass es zwei Arten von Coroutinen gibt: stapellos und stapelbar. Kotlin implementiert stapellose Coroutinen - dies bedeutet, dass Coroutine keinen eigenen Stapel hat und die Möglichkeiten von Coroutine ein wenig einschränken. Eine gute Erklärung können Sie hier lesen .

Beispiele:

  • Stapellos: C #, Scala, Kotlin
  • Stapelbar: Quasar, Javaflow

Was bedeutet es, dass Coroutine wie ein leichter Faden ist?

Dies bedeutet, dass Coroutine in Kotlin keinen eigenen Stack hat, nicht auf einem nativen Thread abgebildet wird und keine Kontextumschaltung auf einem Prozessor erforderlich ist.

Was ist der Unterschied?

Thread - präventiv Multitasking. ( normalerweise ). Coroutine - kooperatives Multitasking.

Thread - wird normalerweise vom Betriebssystem verwaltet. Coroutine - von einem Benutzer verwaltet.

Laufen Kotlins Coroutinen tatsächlich parallel / gleichzeitig?

Es hängt davon ab, ob Sie jede Coroutine in einem eigenen Thread ausführen oder alle Coroutinen in einem Thread oder einem festen Thread-Pool ausführen können.

Mehr darüber, wie Coroutinen ausgeführt werden, finden Sie hier .

Selbst in einem Mehrkernsystem läuft immer nur eine Coroutine (stimmt das?)

Nein, siehe vorherige Antwort.

Hier starte ich 100000 Coroutinen. Was passiert hinter diesem Code?

Eigentlich kommt es darauf an. Angenommen, Sie schreiben den folgenden Code:

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

Dieser Code wird sofort ausgeführt.

Weil wir auf die Ergebnisse des asyncAnrufs warten müssen .

Lassen Sie uns das beheben:

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

        job.join()
    }
}

Wenn Sie dieses Programm ausführen, erstellt kotlin 2 * 100000 Instanzen von Continuation, was einige Dutzend MB RAM beansprucht , und in der Konsole werden Zahlen von 1 bis 100000 angezeigt.

Schreiben wir diesen Code also folgendermaßen um:

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

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

    job.join()
}

Was erreichen wir jetzt? Jetzt erstellen wir nur 100001 Instanzen von Continuation, und das ist viel besser.

Jede erstellte Fortsetzung wird auf CommonPool (einer statischen Instanz von ForkJoinPool) gesendet und ausgeführt.

Ruslan
quelle
15
Tolle Antwort, aber ich würde vorschlagen, eine wichtige Korrektur vorzunehmen. Die Koroutinen in Kotlin verwendet werden Stackless in anfänglicher Pre-Release - Vorschau, sondern tatsächlich in Kotlin 1.1 mit Unterstützung für die Aufhängung an jedem Stapeltiefe, wie in Quasar, beispielsweise freigesetzt. Für diejenigen, die mit Quasar vertraut sind, ist es ziemlich einfach, eine 1: 1-Entsprechung zwischen throws SuspendExecutiondem suspendModifikator von Quasar und Kotlin zu erkennen . Die Implementierungsdetails sind natürlich sehr unterschiedlich, aber die Benutzererfahrung ist ziemlich ähnlich.
Roman Elizarov
5
Sie sind auch willkommen Details Kotlin Koroutinen in dem entsprechenden von der tatsächlichen Implementierung zur Kasse Design - Dokument .
Roman Elizarov
4
Ehrlich gesagt weiß ich nicht, was der Begriff "stapelbare Coroutine" bedeutet. Ich habe keine formale / technische Definition dieses Begriffs gesehen und ich habe verschiedene Leute gesehen, die ihn auf völlig widersprüchliche Weise verwendeten. Ich würde es vermeiden, den Begriff "stapelbare Coroutine" insgesamt zu verwenden. Was ich mit Sicherheit sagen kann und was leicht zu überprüfen ist, ist, dass Kotlin-Coroutinen Quasar viel näher sind und sich sehr von C # unterscheiden. Das Einfügen von Kotlin-Corutinen in denselben Bin wie C # async scheint nicht richtig zu sein, unabhängig von Ihrer speziellen Definition des Wortes "stapelbare Coroutine".
Roman Elizarov
9
Ich würde Coroutinen in verschiedenen Sprachen folgendermaßen klassifizieren: C #, JS usw. haben zukünftige / vielversprechende Coroutinen . Jede asynchrone Berechnung in diesen Sprachen muss eine Art zukunftsähnliches Objekt zurückgeben. Es ist nicht wirklich fair, sie stapellos zu nennen. Sie können asynchrone Berechnungen beliebiger Tiefe ausdrücken, sie sind nur syntaktisch und implementierungsmäßig ineffizient. Kotlin, Quasar usw. haben auf Suspension / Fortsetzung basierende Coroutinen . Sie sind streng leistungsfähiger, da sie mit zukunftsähnlichen Objekten oder ohne sie verwendet werden können, wobei nur Suspendierungsfunktionen verwendet werden.
Roman Elizarov
7
OK. Hier ist ein gutes Papier, das Hintergrundinformationen zu Coroutinen und eine mehr oder weniger genaue Definition von "stapelbarer Coroutine" enthält: inf.puc-rio.br/~roberto/docs/MCC15-04.pdf Dies impliziert, dass Kotlin stapelbare Coroutinen implementiert .
Roman Elizarov
69

Was bedeutet, dass Coroutine wie ein leichter Faden ist?

Coroutine repräsentiert wie ein Thread eine Folge von Aktionen, die gleichzeitig mit anderen Coroutinen (Threads) ausgeführt werden.

Was ist der Unterschied?

Ein Thread ist direkt mit dem nativen Thread im entsprechenden Betriebssystem (Betriebssystem) verbunden und verbraucht eine beträchtliche Menge an Ressourcen. Insbesondere verbraucht es viel Speicher für seinen Stapel. Deshalb können Sie nicht einfach 100.000 Threads erstellen. Es ist wahrscheinlich, dass Ihnen der Speicher ausgeht. Das Wechseln zwischen Threads erfordert einen OS-Kernel-Dispatcher und ist in Bezug auf die verbrauchten CPU-Zyklen ein ziemlich teurer Vorgang.

Eine Coroutine hingegen ist eine reine Sprachabstraktion auf Benutzerebene. Es bindet keine nativen Ressourcen und verwendet im einfachsten Fall nur ein relativ kleines Objekt im JVM-Heap. Aus diesem Grund ist es einfach, 100.000 Coroutinen zu erstellen. Das Wechseln zwischen Coroutinen beinhaltet überhaupt keinen Betriebssystemkern. Es kann so billig sein wie das Aufrufen einer regulären Funktion.

Laufen Kotlins Coroutinen tatsächlich parallel / gleichzeitig? Selbst in Mehrkernsystemen läuft immer nur eine Coroutine (stimmt das?)

Eine Coroutine kann entweder ausgeführt oder ausgesetzt werden. Eine angehaltene Coroutine ist keinem bestimmten Thread zugeordnet, aber eine laufende Coroutine wird auf einem Thread ausgeführt (die Verwendung eines Threads ist die einzige Möglichkeit, etwas innerhalb eines Betriebssystemprozesses auszuführen). Ob verschiedene Coroutinen alle auf demselben Thread ausgeführt werden (a verwendet möglicherweise nur eine einzige CPU in einem Multicore-System) oder in unterschiedlichen Threads (und somit möglicherweise mehrere CPUs), liegt ausschließlich in den Händen eines Programmierers, der Coroutinen verwendet.

In Kotlin wird der Versand von Coroutinen über den Coroutine-Kontext gesteuert . Weitere Informationen finden Sie im Handbuch zu kotlinx.coroutines

Hier starte ich 100000 Coroutinen. Was passiert hinter diesem Code?

Angenommen, Sie verwenden launchFunktion und CommonPoolKontext aus dem kotlinx.coroutinesProjekt (Open Source), können Sie den Quellcode hier untersuchen:

Das launcherstellt nur eine neue Coroutine, während CommonPoolCoroutinen an eine gesendet werden, ForkJoinPool.commonPool()die mehrere Threads verwendet und daher in diesem Beispiel auf mehreren CPUs ausgeführt wird.

Der Code, der auf den launchAufruf von folgt, {...}wird als suspendierendes Lambda bezeichnet . Was ist das und wie umgesetzt wird gesperrt , lambdas und Funktionen (kompilierten) sowie Standard - Library - Funktionen und Klassen mögen startCoroutines, suspendCoroutineund CoroutineContextin dem entsprechenden erklärt Kotlin Koroutinen Designdokument .

Roman Elizarov
quelle
3
Bedeutet das grob gesagt, dass das Starten einer Couroutine dem Hinzufügen eines Jobs zu einer Thread-Warteschlange ähnelt, in der die Thread-Warteschlange vom Benutzer gesteuert wird?
Leo
2
Ja. Dies kann eine Warteschlange für einen einzelnen Thread oder eine Warteschlange für einen Thread-Pool sein. Sie können Coroutinen als übergeordnetes Grundelement anzeigen, mit dem Sie vermeiden können, Fortsetzungen Ihrer Geschäftslogik manuell (erneut) an die Warteschlange zu senden.
Roman Elizarov
Bedeutet das nicht, dass wenn mehrere Coroutinen parallel ausgeführt werden, dies keine echte Parallelität ist, wenn die Anzahl der Coroutinen viel größer ist als die Thread-Anzahl der Threads in der Warteschlange? Wenn das der Fall ist, dann klingt das wirklich ähnlich wie bei Java. Gibt Executores eine Beziehung zwischen diesen beiden?
Leo
6
Das unterscheidet sich nicht von Threads. Wenn die Anzahl der Threads größer ist als die Anzahl der physischen Kerne, ist dies keine echte Parallelität. Der Unterschied besteht darin, dass Threads präventiv auf Kernen geplant werden , während Coroutinen kooperativ
Roman Elizarov