Themen gegen Coroutinen in Kotlin

1. Einleitung

In diesem kurzen Tutorial werden wir Threads in Kotlin erstellen und ausführen.

Später werden wir diskutieren, wie dies zugunsten von Kotlin Coroutines insgesamt vermieden werden kann.

2. Threads erstellen

Das Erstellen eines Threads in Kotlin ähnelt dem Erstellen in Java.

Wir könnten entweder die Thread- Klasse erweitern (obwohl dies nicht empfohlen wird, da Kotlin keine Mehrfachvererbung unterstützt):

class SimpleThread: Thread() { public override fun run() { println("${Thread.currentThread()} has run.") } }

Oder wir können die Runnable- Schnittstelle implementieren :

class SimpleRunnable: Runnable { public override fun run() { println("${Thread.currentThread()} has run.") } }

Und genauso wie in Java können wir es ausführen, indem wir die start () -Methode aufrufen :

val thread = SimpleThread() thread.start() val threadWithRunnable = Thread(SimpleRunnable()) threadWithRunnable.start()

Alternativ unterstützt Kotlin wie Java 8 SAM-Konvertierungen, daher können wir diese nutzen und ein Lambda übergeben:

val thread = Thread { println("${Thread.currentThread()} has run.") } thread.start()

2.2. Kotlin thread () Funktion

Eine andere Möglichkeit besteht darin , den von Kotlin bereitgestellten Funktionsthread () zu berücksichtigen :

fun thread( start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit ): Thread

Mit dieser Funktion kann ein Thread instanziiert und einfach ausgeführt werden durch:

thread(start = true) { println("${Thread.currentThread()} has run.") }

Die Funktion akzeptiert fünf Parameter:

  • start - Um den Thread sofort auszuführen
  • isDaemon - Zum Erstellen des Threads als Daemon-Thread
  • contextClassLoader - Ein Klassenladeprogramm zum Laden von Klassen und Ressourcen
  • name - Zum Festlegen des Namens des Threads
  • priority - Zum Festlegen der Priorität des Threads

3. Kotlin Coroutines

Es ist verlockend zu glauben, dass das Laichen von mehr Threads uns helfen kann, mehr Aufgaben gleichzeitig auszuführen. Das stimmt leider nicht immer.

Das Erstellen zu vieler Threads kann in einigen Situationen zu einer schlechten Leistung einer Anwendung führen. Threads sind Objekte, die während der Objektzuweisung und der Speicherbereinigung Overhead verursachen.

Um diese Probleme zu lösen, führte Kotlin eine neue Methode zum Schreiben von asynchronem, nicht blockierendem Code ein. die Coroutine.

Ähnlich wie bei Threads können Coroutinen gleichzeitig ausgeführt werden, warten und miteinander kommunizieren, mit dem Unterschied, dass das Erstellen dieser Threads weitaus billiger ist als das von Threads.

3.1. Coroutine-Kontext

Bevor wir die Coroutine-Builder vorstellen, die Kotlin standardmäßig bereitstellt, müssen wir den Coroutine-Kontext diskutieren.

Coroutinen werden immer in einem Kontext ausgeführt, der aus einer Reihe verschiedener Elemente besteht.

Die Hauptelemente sind:

  • Job - Modelliert einen stornierbaren Workflow mit mehreren Status und einem Lebenszyklus, der in seiner Fertigstellung gipfelt
  • Dispatcher - Bestimmt, welchen Thread oder welche Threads die entsprechende Coroutine für ihre Ausführung verwendet. Mit dem Dispatcher können wir die Coroutine-Ausführung auf einen bestimmten Thread beschränken, ihn an einen Thread-Pool senden oder ihn unbeschränkt ausführen lassen

Wir werden sehen, wie der Kontext angegeben wird, während wir die Coroutinen in den nächsten Schritten beschreiben.

3.2. starten

Die Start - Funktion ist ein Koroutine Builder , die einen neuen Koroutine beginnt , ohne den aktuellen Thread blockiert und gibt eine Referenz auf das Koroutine als Job Objekt:

runBlocking { val job = launch(Dispatchers.Default) { println("${Thread.currentThread()} has run.") } }

Es gibt zwei optionale Parameter:

  • Kontext - Der Kontext, in dem die Coroutine ausgeführt wird. Wenn sie nicht definiert ist, erbt sie den Kontext von dem CoroutineScope, von dem aus sie gestartet wird
  • start - Die Startoptionen für die Coroutine. Standardmäßig wird die Coroutine sofort zur Ausführung eingeplant

Beachten Sie, dass der obige Code in einem gemeinsam genutzten Hintergrundpool von Threads ausgeführt wird, da wir Dispatchers.Default verwendet haben, das ihn in GlobalScope startet .

Alternativ können wir GlobalScope.launch verwenden, das denselben Dispatcher verwendet:

val job = GlobalScope.launch { println("${Thread.currentThread()} has run.") }

Wenn wir Dispatchers.Default oder GlobalScope.launch verwenden , erstellen wir eine Coroutine der obersten Ebene. Obwohl es leicht ist, verbraucht es während der Ausführung einige Speicherressourcen.

Anstatt Coroutinen in GlobalScope zu starten, wie wir es normalerweise mit Threads tun (Threads sind immer global), können wir Coroutinen in dem spezifischen Umfang der Operation starten, die wir ausführen:

runBlocking { val job = launch { println("${Thread.currentThread()} has run.") } }

In diesem Fall starten wir eine neue Coroutine im runBlocking Coroutine Builder (den wir später beschreiben werden), ohne den Kontext anzugeben. Somit erbt die Coroutine den Kontext von runBlocking .

3.3. asynchron

Eine weitere Funktion, die Kotlin zum Erstellen einer Coroutine bereitstellt, ist die asynchrone Funktion .

Die asynchrone Funktion erstellt eine neue Coroutine und gibt ein zukünftiges Ergebnis als Instanz von Deferred zurück:

val deferred = async { [email protected] "${Thread.currentThread()} has run." }

deferred is a non-blocking cancellable future which describes an object that acts as a proxy for a result that is initially unknown.

Like launch, we can specify a context in which to execute the coroutine as well as a start option:

val deferred = async(Dispatchers.Unconfined, CoroutineStart.LAZY) { println("${Thread.currentThread()} has run.") }

In this case, we've launched the coroutine using the Dispatchers.Unconfined which starts coroutines in the caller thread but only until the first suspension point.

Note that Dispatchers.Unconfined is a good fit when a coroutine does not consume CPU time nor updates any shared data.

In addition, Kotlin provides Dispatchers.IO that uses a shared pool of on-demand created threads:

val deferred = async(Dispatchers.IO) { println("${Thread.currentThread()} has run.") }

Dispatchers.IO is recommended when we need to do intensive I/O operations.

3.4. runBlocking

We had an earlier look at runBlocking, but now let's talk about it in more depth.

runBlocking is a function that runs a new coroutine and blocks the current thread until its completion.

By way of example in the previous snippet, we launched the coroutine but we never waited for the result.

In order to wait for the result, we have to call the await() suspend method:

// async code goes here runBlocking { val result = deferred.await() println(result) }

await() is what’s called a suspend function. Suspend functions are only allowed to be called from a coroutine or another suspend function. For this reason, we have enclosed it in a runBlocking invocation.

Wir verwenden runBlocking in Haupt- Funktionen und in Tests , damit wir verknüpfen können auf andere geschrieben Sperrung Code - Stil in suspendieren.

Auf ähnliche Weise wie bei anderen Coroutine-Buildern können wir den Ausführungskontext festlegen:

runBlocking(newSingleThreadContext("dedicatedThread")) { val result = deferred.await() println(result) }

Beachten Sie, dass wir einen neuen Thread erstellen können, in dem wir die Coroutine ausführen können. Ein dedizierter Thread ist jedoch eine teure Ressource. Und wenn es nicht mehr benötigt wird, sollten wir es freigeben oder noch besser in der gesamten Anwendung wiederverwenden.

4. Fazit

In diesem Tutorial haben wir gelernt, wie man asynchronen, nicht blockierenden Code durch Erstellen eines Threads ausführt.

Als Alternative zum Thread haben wir auch gesehen, wie einfach und elegant Kotlins Ansatz zur Verwendung von Coroutinen ist.

Wie üblich sind alle in diesem Tutorial gezeigten Codebeispiele auf Github verfügbar.