Discussioni contro Coroutines a Kotlin

1. Introduzione

In questo breve tutorial, creeremo ed eseguiremo thread in Kotlin.

Più avanti, discuteremo come evitarlo del tutto, a favore di Kotlin Coroutines.

2. Creazione di thread

La creazione di un thread in Kotlin è simile a farlo in Java.

Potremmo estendere la classe Thread (anche se non è consigliata perché Kotlin non supporta l'ereditarietà multipla):

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

Oppure possiamo implementare l' interfaccia Runnable :

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

E nello stesso modo in cui facciamo in Java, possiamo eseguirlo chiamando il metodo start () :

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

In alternativa, come Java 8, Kotlin supporta le conversioni SAM, quindi possiamo trarne vantaggio e passare un lambda:

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

2.2. Funzione Kotlin thread ()

Un altro modo è considerare la funzione thread () fornita da Kotlin:

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

Con questa funzione, un thread può essere istanziato ed eseguito semplicemente:

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

La funzione accetta cinque parametri:

  • start - Per eseguire immediatamente il thread
  • isDaemon - Per creare il thread come thread daemon
  • contextClassLoader - Un programma di caricamento classi da utilizzare per caricare classi e risorse
  • nome - Per impostare il nome del thread
  • priorità - Per impostare la priorità del thread

3. Kotlin Coroutines

Si è tentati di pensare che la generazione di più thread possa aiutarci a eseguire più attività contemporaneamente. Purtroppo non è sempre vero.

La creazione di troppi thread può effettivamente rendere un'applicazione insufficiente in alcune situazioni; i thread sono oggetti che impongono un sovraccarico durante l'allocazione degli oggetti e la garbage collection.

Per superare questi problemi, Kotlin ha introdotto un nuovo modo di scrivere codice asincrono e non bloccante; la Coroutine.

Analogamente ai thread, le coroutine possono essere eseguite contemporaneamente, attendere e comunicare tra loro con la differenza che crearle è molto più economico dei thread.

3.1. Contesto coroutine

Prima di presentare i costruttori di coroutine che Kotlin fornisce fuori dagli schemi, dobbiamo discutere il contesto di Coroutine.

Le coroutine vengono sempre eseguite in un contesto che è un insieme di vari elementi.

Gli elementi principali sono:

  • Lavoro: modella un flusso di lavoro annullabile con più stati e un ciclo di vita che culmina nel suo completamento
  • Dispatcher: determina quale thread o thread la coroutine corrispondente utilizza per la sua esecuzione. Con il dispatcher, possiamo limitare l'esecuzione della coroutine a un thread specifico, inviarla a un pool di thread o lasciarla eseguire senza limiti

Vedremo come specificare il contesto mentre descriveremo le coroutine nelle prossime fasi.

3.2. lanciare

La funzione di avvio è un generatore di coroutine che avvia una nuova coroutine senza bloccare il thread corrente e restituisce un riferimento alla coroutine come oggetto Job :

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

Ha due parametri opzionali:

  • contesto - Il contesto in cui viene eseguita la coroutine, se non definito, eredita il contesto dal CoroutineScope da cui viene avviata
  • start - Le opzioni di avvio per la coroutine. Per impostazione predefinita, la coroutine viene immediatamente pianificata per l'esecuzione

Si noti che il codice precedente viene eseguito in un pool di thread in background condiviso perché abbiamo utilizzato Dispatchers.Default che lo avvia in GlobalScope.

In alternativa, possiamo utilizzare GlobalScope.launch che utilizza lo stesso dispatcher:

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

Quando usiamo Dispatchers.Default o GlobalScope.launch creiamo una coroutine di primo livello. Anche se è leggero, consuma comunque alcune risorse di memoria durante il funzionamento.

Invece di avviare le coroutine nel GlobalScope, proprio come facciamo di solito con i thread (i thread sono sempre globali), possiamo lanciare le coroutine nell'ambito specifico dell'operazione che stiamo eseguendo:

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

In questo caso, iniziamo una nuova coroutine all'interno del coroutine builder runBlocking (che descriveremo in seguito) senza specificare il contesto. Pertanto, la coroutine erediterà il contesto di runBlocking .

3.3. asincrono

Un'altra funzione che Kotlin fornisce per creare una coroutine è asincrona .

La funzione async crea una nuova coroutine e restituisce un risultato futuro come istanza di Deferred:

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.

Usiamo runBlocking nelle funzioni principali e nei test in modo da poter collegare il codice di blocco ad altri scritti in stile suspending.

In modo simile a come abbiamo fatto in altri costruttori di coroutine, possiamo impostare il contesto di esecuzione:

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

Nota che possiamo creare un nuovo thread in cui possiamo eseguire la coroutine. Tuttavia, un thread dedicato è una risorsa costosa. E, quando non è più necessario, dovremmo rilasciarlo o ancora meglio riutilizzarlo in tutta l'applicazione.

4. Conclusione

In questo tutorial abbiamo appreso come eseguire codice asincrono e non bloccante creando un thread.

In alternativa al thread, abbiamo anche visto come l'approccio di Kotlin all'uso delle coroutine sia semplice ed elegante.

Come al solito, tutti gli esempi di codice mostrati in questo tutorial sono disponibili su Github.