Discussies versus Coroutines in Kotlin

1. Inleiding

In deze korte tutorial gaan we threads maken en uitvoeren in Kotlin.

Later zullen we bespreken hoe we het helemaal kunnen vermijden, in het voordeel van Kotlin Coroutines.

2. Discussies maken

Het maken van een thread in Kotlin is vergelijkbaar met het maken van een thread in Java.

We kunnen het Draad class (hoewel het niet wordt aanbevolen omdat Kotlin geen meervoudige overerving ondersteunt):

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

Of we kunnen het Runnable koppel:

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

En op dezelfde manier als we doen in Java, kunnen we het uitvoeren door de begin() methode:

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

Als alternatief ondersteunt Kotlin, net als Java 8, SAM-conversies, daarom kunnen we hiervan profiteren en een lambda doorgeven:

val thread = Thread {println ("$ {Thread.currentThread ()} is uitgevoerd.")} thread.start ()

2.2. Kotlin draad() Functie

Een andere manier is om naar de functie te kijken draad() dat Kotlin biedt:

leuke thread (start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, naam: String? = null, prioriteit: Int = -1, block: () -> Unit): Thread

Met deze functie kan een thread worden geïnstantieerd en eenvoudig worden uitgevoerd door:

thread (start = true) {println ("$ {Thread.currentThread ()} is uitgevoerd.")}

De functie accepteert vijf parameters:

  • begin - Om meteen de draad uit te voeren
  • isDaemon - Om de thread te maken als een daemon-thread
  • contextClassLoader - Een klassenlader om te gebruiken voor het laden van klassen en bronnen
  • naam - Om de naam van de draad in te stellen
  • prioriteit - Om de prioriteit van de draad in te stellen

3. Kotlin Coroutines

Het is verleidelijk om te denken dat het spawnen van meer threads ons kan helpen meer taken tegelijkertijd uit te voeren. Helaas is dat niet altijd waar.

Als u te veel threads maakt, kan een toepassing in sommige situaties zelfs ondermaats presteren; threads zijn objecten die overhead opleggen tijdens objecttoewijzing en garbage collection.

Om deze problemen op te lossen, Kotlin introduceerde een nieuwe manier om asynchrone, niet-blokkerende code te schrijven; de Coroutine.

Net als bij threads, kunnen coroutines gelijktijdig worden uitgevoerd, wachten en met elkaar communiceren, met het verschil dat het maken ervan veel goedkoper is dan threads.

3.1. Coroutine Context

Voordat we de coroutine-builders presenteren die Kotlin out-of-the-box levert, moeten we de Coroutine-context bespreken.

Coroutines worden altijd uitgevoerd in een context die een set van verschillende elementen is.

De belangrijkste elementen zijn:

  • Job - modelleert een annuleerbare workflow met meerdere statussen en een levenscyclus die culmineert in de voltooiing ervan
  • Dispatcher - bepaalt welke thread of threads de corresponderende coroutine gebruikt voor zijn uitvoering. Met de coördinator, we kunnen een coroutine-uitvoering beperken tot een specifieke thread, deze naar een threadpool verzenden of deze onbeperkt laten draaien

We zullen zien hoe we de context kunnen specificeren terwijl we de coroutines in de volgende fasen beschrijven.

3.2. lancering

De lancering function is een coroutine-builder die een nieuwe coroutine start zonder de huidige thread te blokkeren en retourneert een verwijzing naar de coroutine als een Job voorwerp:

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

Het heeft twee optionele parameters:

  • context - De context waarin de coroutine wordt uitgevoerd, indien niet gedefinieerd, erft de context van de CoroutineScope het wordt gelanceerd vanaf
  • begin - De startopties voor de coroutine. Standaard wordt de coroutine onmiddellijk gepland voor uitvoering

Merk op dat de bovenstaande code wordt uitgevoerd in een gedeelde achtergrondpool van threads omdat we Dispatchers. Standaard die het lanceert in de GlobalScope.

Als alternatief kunnen we gebruiken GlobalScope.launch die dezelfde dispatcher gebruikt:

val job = GlobalScope.launch {println ("$ {Thread.currentThread ()} is uitgevoerd.")}

Wanneer we gebruiken Dispatchers. Standaard of GlobalScope.launch we creëren een coroutine op het hoogste niveau. Hoewel het licht van gewicht is, verbruikt het nog steeds wat geheugenbronnen terwijl het wordt uitgevoerd.

In plaats van coroutines te lanceren in de GlobalScope, zoals we gewoonlijk doen met threads (threads zijn altijd globaal), kunnen we coroutines lanceren in de specifieke scope van de operatie die we uitvoeren:

runBlocking {val job = launch {println ("$ {Thread.currentThread ()} is uitgevoerd.")}}

In dit geval beginnen we met nieuwe coroutine in de runBlocking coroutine builder (die we later zullen beschrijven) zonder de context op te geven. Dus de coroutine zal erven runBlocking'S context.

3.3. asynch

Een andere functie die Kotlin biedt om een ​​coroutine te maken, is asynch.

De asynch functie maakt een nieuwe coroutine aan en retourneert een toekomstig resultaat als een instantie van Uitgesteld:

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

uitgesteld is een niet-blokkerende opzegbare toekomst die een object beschrijft dat fungeert als een proxy voor een resultaat dat aanvankelijk onbekend is.

Leuk vinden lancering, we kunnen een context specificeren waarin de coroutine moet worden uitgevoerd, evenals een startoptie:

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

In dit geval hebben we de coroutine gelanceerd met behulp van de Verzenders.Onbeperkt die coroutines in de caller-thread begint, maar alleen tot het eerste opschortingspunt.

Let daar op Verzenders.Onbeperkt past goed wanneer een coroutine geen CPU-tijd verbruikt en geen gedeelde gegevens bijwerkt.

Daarnaast biedt Kotlin Dispatchers.IO die een gedeelde pool van op aanvraag gemaakte threads gebruikt:

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

Dispatchers.IO wordt aanbevolen wanneer we intensieve I / O-bewerkingen moeten uitvoeren.

3.4. runBlocking

We hadden een eerdere blik op runBlocking, maar laten we er nu dieper op ingaan.

runBlocking is een functie die een nieuwe coroutine uitvoert en de huidige thread blokkeert totdat deze is voltooid.

Bij wijze van voorbeeld in het vorige fragment, hebben we de coroutine gelanceerd, maar we hebben nooit op het resultaat gewacht.

Om op het resultaat te wachten, moeten we de wachten() opschorten methode:

// async-code komt hier runBlocking {val result = deferred.await () println (resultaat)}

wachten() is wat een onderbrekingsfunctie wordt genoemd. Suspend-functies mogen alleen worden aangeroepen vanuit een coroutine of een andere suspend-functie. Om deze reden hebben we het ingesloten in een runBlocking aanroeping.

We gebruiken runBlocking in hoofd functies en in tests, zodat we blokkeringscode kunnen koppelen aan andere geschreven in opschortende stijl.

Op dezelfde manier als bij andere coroutine-builders, kunnen we de uitvoeringscontext instellen:

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

Merk op dat we een nieuwe thread kunnen maken waarin we de coroutine kunnen uitvoeren. Een speciale thread is echter een dure bron. En als het niet langer nodig is, moeten we het vrijgeven of zelfs beter hergebruiken gedurende de hele applicatie.

4. Conclusie

In deze zelfstudie hebben we geleerd hoe we asynchrone, niet-blokkerende code kunnen uitvoeren door een thread te maken.

Als alternatief voor de thread hebben we ook gezien hoe Kotlins benadering van het gebruik van coroutines eenvoudig en elegant is.

Zoals gewoonlijk zijn alle codevoorbeelden die in deze tutorial worden getoond, beschikbaar op Github.