Concurrency and Coroutines in Kotlin

Java Virtual Machine, o JVM in breve, supporta il multithreading. Qualsiasi processo eseguito su di esso è libero di creare un numero ragionevole di thread per eseguire più attività in modo asincrono. Tuttavia, scrivere codice in grado di farlo in modo ottimale e privo di errori può essere estremamente difficile. Nel corso degli anni, Java, altre lingue JVM e un sacco di librerie di terze parti hanno cercato di proporre approcci creativi ed eleganti per affrontare questo problema.

Ad esempio, Java 5 ha introdotto il framework executor, che consente di disaccoppiare i dettagli di gestione dei thread dalla logica aziendale. Java 8 offre flussi paralleli, che possono essere facilmente utilizzati con espressioni lambda. RxJava porta estensioni reattive a Java, consentendo di scrivere codice asincrono molto conciso e leggibile.

Kotlin supporta quasi tutti questi approcci e ne offre alcuni. In questo tutorial, ti mostrerò come utilizzarli nelle app Android.

Prerequisiti

Per poter seguire questo tutorial, avrai bisogno di:

  • Android Studio 2.3.3 o versioni successive
  • Plugin Kotlin 1.1.51 o successivo
  • una conoscenza di base dei thread Java

Se non ti piace lavorare con le espressioni lambda e le interfacce SAM, ti suggerisco di leggere il seguente tutorial prima di procedere:

Puoi anche imparare tutti i dettagli del linguaggio Kotlin nella nostra serie Kotlin From Scratch.

  • Kotlin From Scratch: Nullability, Loops e Conditions

    Kotlin è gratuito e open source e rende ancora più divertente la programmazione per Android. In questo tutorial, guarderemo a valori nulla, loop e condizioni in Kotlin.
    Chike Mgbemena
    Kotlin
  • Kotlin From Scratch: più divertimento con le funzioni

    Informazioni su funzioni di primo livello, espressioni lambda, funzioni anonime, funzioni locali, funzioni infissi e funzioni membro in Kotlin.
    Chike Mgbemena
    Kotlin

1. Creazione di thread

Di solito, istanze di classi che implementano il Runnable l'interfaccia è usata per creare discussioni in Kotlin. Perché il Runnable l'interfaccia ha solo un metodo, il correre() metodo, è possibile sfruttare la funzionalità di conversione SAM di Kotlin per creare nuovi thread con codice minimo di codice.

Ecco come puoi usare il filo() funzione, che fa parte della libreria standard di Kotlin, per creare e avviare rapidamente un nuovo thread:

thread // qualche operazione di lunga durata

L'approccio sopra riportato è appropriato solo quando è necessario generare occasionalmente un thread o due. Se la concorrenza è una parte importante della logica di business della tua app e hai bisogno di un numero elevato di thread, l'utilizzo di pool di thread con un servizio executor è un'idea migliore. 

Ad esempio, il seguente codice utilizza il newFixedThreadPool () metodo del esecutori class per creare un pool di thread contenente otto thread riutilizzabili e che esegue un numero elevato di operazioni in background su di esso:

val myService: ExecutorService = Executors.newFixedThreadPool (8) var i = 0 while (i < items.size)  // items may be a large array val item = items[i] myService.submit  processItem(item) // a long running operation  i += 1 

Potrebbe non essere ovvio a prima vista ma, nel codice precedente, l'argomento del Sottoscrivi() il metodo del servizio executor è in realtà a Runnable oggetto.

2. Ottenere risultati dai thread

Attività in background create usando il Runnable l'interfaccia non può restituire alcun risultato direttamente. Se si desidera ricevere risultati dai thread, è necessario utilizzare il callable interfaccia invece, che è anche un'interfaccia SAM.

Quando passi a callable oggetto al Sottoscrivi() metodo di un servizio esecutore, si riceve un Futuro oggetto. Come suggerisce il nome, il Futuro l'oggetto conterrà il risultato del callable ad un certo punto nel futuro, quando il servizio dell'esecutore ha terminato di eseguirlo. Per ottenere il risultato effettivo da a Futuro oggetto, tutto ciò che devi fare è chiamare il suo ottenere() metodo, ma attenzione, il thread si bloccherà se lo chiamate prematuramente.

Il seguente codice di esempio mostra come creare un callable oggetto che restituisce a Futuro di tipo Stringa, eseguilo e stampa il risultato:

val myService: ExecutorService = Executors.newFixedThreadPool (2) val result = myService.submit (Callable // qualche operazione in background che genera // a string) // Altre operazioni // Stampa risultato Log.d (TAG, result.get ())

3. Sincronizzazione dei thread

A differenza di Java, Kotlin non ha il sincronizzato parola chiave. Pertanto, per sincronizzare più operazioni in background, è previsto l'uso di @Synchronized annotazione o il sincronizzato () funzione inline della libreria standard. L'annotazione può sincronizzare un intero metodo e la funzione funziona su un blocco di istruzioni.

// una funzione sincronizzata @Synchronized fun myFunction ()  fun myOtherFunction () // un blocco sincronizzato sincronizzato (this) 

Sia il @Synchronized annotazione e il sincronizzato () la funzione usa il concetto di serrature del monitor. 

Se non lo sai già, ad ogni oggetto sulla JVM è associato un monitor. Per ora, puoi pensare a un monitor come a un token speciale che un thread può acquisire o bloccare per ottenere l'accesso esclusivo all'oggetto. Una volta che il monitor di un oggetto è bloccato, gli altri thread che vogliono lavorare sull'oggetto dovranno aspettare fino a che il monitor non verrà rilasciato, o sbloccato, di nuovo.

Mentre il @Synchronized annotazione blocca il monitor dell'oggetto a cui appartiene il metodo associato, il sincronizzato () la funzione può bloccare il monitor di qualsiasi oggetto che gli viene passato come argomento.

4. Capire le coroutine

Attraverso una libreria sperimentale, Kotlin offre un approccio alternativo per raggiungere la concorrenza: le coroutine. Le coroutine sono molto più leggere dei fili e sono molto più facili da gestire.

Nelle applicazioni multithread mobili, i thread vengono in genere utilizzati per operazioni come il recupero di informazioni da Internet o l'interrogazione di database. Tali operazioni non implicano molto calcolo e le discussioni trascorrono la maggior parte della loro vita in uno stato bloccato, aspettando solo che i dati vengano da qualche altra parte. Come probabilmente puoi dire, non è un modo molto efficiente di usare la CPU.

Le coroutine sono progettate per essere utilizzate al posto dei fili per tali operazioni. La cosa più importante da capire sulle coroutine è che sono sospensibili. In altre parole, invece di bloccare, possono semplicemente fermarsi quando necessario e continuare senza problemi in seguito. Questo porta a un utilizzo della CPU molto migliore. Infatti, con coroutine accuratamente progettate, è possibile eseguire senza sforzo dozzine di operazioni in background.

Per poter utilizzare le coroutine nel tuo progetto Android Studio, assicurati di aggiungere quanto segue compilare dipendenza nel App modulo di build.gradle file:

compila 'org.jetbrains.kotlinx: kotlinx-coroutines-android: 0.19.3'

5. Creazione di funzioni di sospensione

Una coroutine può essere sospesa solo con l'aiuto di una funzione di sospensione. Pertanto, la maggior parte delle coroutine hanno chiamate ad almeno una di queste funzioni al loro interno.

Per creare una funzione di sospensione, tutto ciò che devi fare è aggiungere il sospendere modificatore a una funzione regolare. Ecco una tipica funzione di sospensione che esegue una richiesta HTTP GET usando la libreria khttp:

sospendi fun fetchWebsiteContents (url: String): String return khttp.get (url) .text

Si noti che una funzione di sospensione può essere chiamata solo da una coroutine o da un'altra funzione di sospensione. Se provi a chiamarlo da qualsiasi altra parte, il tuo codice non riuscirà a compilare.

6. Creazione di coroutine

Quando si tratta di creare una nuova coroutine, la libreria standard di Kotlin ha abbastanza costruttori di coroutine per farti sentire l'imbarazzo della scelta. Il più semplice costruttore di coroutine che puoi usare è il lanciare() funzione, e come la maggior parte degli altri costruttori di coroutine, si aspetta una sospensione lambda, che non è altro che una funzione di sospensione anonima. Come tale, questa lambda è ciò che diventa la coroutine.

Il codice seguente crea una coroutine che effettua due chiamate sequenziali alla funzione di sospensione che abbiamo creato nel passaggio precedente:

val job1 = launch val website1 = fetchWebsiteContents ("https://code.tutsplus.com") val website2 = fetchWebsiteContents ("https://design.tutsplus.com")

Il valore di ritorno del lanciare() la funzione è a Lavoro oggetto, che è possibile utilizzare per gestire la coroutine. Ad esempio, puoi chiamarlo aderire() metodo per attendere il completamento della coroutine. Allo stesso modo, puoi chiamarlo Annulla() metodo per annullare immediatamente la coroutine.

Usando il lanciare() la funzione è molto simile alla creazione di una nuova discussione con a Runnable oggetto, principalmente perché non è possibile restituire alcun valore da esso. Se vuoi essere in grado di restituire un valore dalla tua coroutine, devi crearlo usando il async () funzione invece.

Il async () la funzione restituisce a differite oggetto, che, proprio come il Lavoro oggetto, ti consente di gestire la coroutine. Tuttavia, ti permette anche di usare il attendere () funzione per attendere il risultato della coroutine senza bloccare il thread corrente.

Ad esempio, considera le seguenti coroutine che usano il fetchWebsiteContents () sospendere la funzione e restituire la lunghezza del contenuto di due indirizzi di pagine Web diversi:

val jobForLength1 = async fetchWebsiteContents ("https://webdesign.tutsplus.com") .length val jobForLength2 = async fetchWebsiteContents ("https://photography.tutsplus.com") .length

Con il codice sopra, entrambe le coroutine inizieranno immediatamente e correranno in parallelo.

Se ora desideri utilizzare le lunghezze restituite, devi chiamare il attendere () metodo su entrambi i differite oggetti. Tuttavia, perché il attendere () anche il metodo è una funzione sospesa, devi assicurarti di chiamarlo da un'altra coroutine.

Il codice seguente mostra come calcolare la somma delle due lunghezze usando una nuova coroutine creata con lanciare() funzione:

launch val sum = jobForLength1.await () + jobForLength2.await () println ("Download $ sum bytes!")

7. Utilizzo di coroutine nel thread UI

Le Coroutine fanno uso internamente di thread in background, motivo per cui non vengono eseguiti su thread di interfaccia utente di un'app Android per impostazione predefinita. Di conseguenza, se provi a modificare i contenuti dell'interfaccia utente della tua app dall'interno di una coroutine, troverai un errore di runtime. Fortunatamente, è banalmente facile eseguire una coroutine sul thread dell'interfaccia utente: devi solo passare il UI oggetto come argomento per il tuo costruttore di coroutine.

Ad esempio, ecco come riscrivere l'ultima coroutine per visualizzare la somma all'interno di a TextView widget di:

launch (UI) val sum = jobForLength1.await () + jobForLength2.await () myTextView.text = "Download $ sum bytes!" 

Il codice sopra può sembrare banale all'inizio, ma guarda di nuovo. Non solo è in grado di attendere il completamento di due operazioni in background senza utilizzare i callback, ma è in grado di farlo sul thread dell'interfaccia utente dell'applicazione senza bloccarlo!

Avere la possibilità di attendere sul thread dell'interfaccia utente, senza che l'interfaccia utente si senta lenta o che si inneschi un errore di non risposta dell'applicazione, spesso definito ANR, semplifica molte attività altrimenti complesse. 

Ad esempio, con la sospensione ritardo() funzione, che è l'equivalente non bloccante del Thread.sleep () metodo, ora puoi creare animazioni con loop. Per aiutarti a iniziare, ecco una coroutine di esempio che incrementa la coordinata x di a TextView widget ogni 400 ms, creando così un effetto di tipo tendone:

launch (UI) while (myTextView.x < 800)  myTextView.x += 10 delay(400)  

Conclusione

Durante lo sviluppo di app Android, è imperativo eseguire operazioni di lunga durata in thread in background. In questo tutorial, hai appreso diversi approcci che puoi seguire per creare e gestire tali thread in Kotlin. Hai anche imparato a utilizzare la funzionalità di coroutine ancora sperimentale per attendere i thread senza bloccarli.

Per saperne di più sulle coroutine, puoi fare riferimento alla documentazione ufficiale. E mentre sei qui, controlla alcuni dei nostri altri post su Kotlin e lo sviluppo di Android!