Sean McQuillan
Sean McQuillan

Follow

Apr 30, 2019 – 9 min read

Questo fa parte di una serie in più parti sull’uso delle coroutine su Android. Questo post si concentra su come funzionano le coroutine e quali problemi risolvono.

Altri articoli di questa serie:

Le coroutine Kotlin introducono un nuovo stile di concorrenza che può essere usato su Android per semplificare il codice asincrono. Mentre sono nuove per Kotlin nella 1.3, il concetto di coroutine è stato in giro fin dagli albori dei linguaggi di programmazione. Il primo linguaggio ad esplorare l’uso delle coroutine è stato Simula nel 1967.

Negli ultimi anni, le coroutine sono cresciute in popolarità e sono ora incluse in molti linguaggi di programmazione popolari come Javascript, C#, Python, Ruby e Go per nominarne alcuni. Le coroutine di Kotlin si basano su concetti consolidati che sono stati utilizzati per costruire applicazioni di grandi dimensioni.

Su Android, le coroutine sono una grande soluzione a due problemi:

  1. I compiti a lunga esecuzione sono compiti che richiedono troppo tempo per bloccare il thread principale.
  2. La Main-safety permette di garantire che qualsiasi funzione di sospensione possa essere chiamata dal thread principale.

Tuffiamoci in ognuna di esse per vedere come le coroutine possono aiutarci a strutturare il codice in modo più pulito!

Long running tasks

Il recupero di una pagina web o l’interazione con un’API implicano entrambi una richiesta di rete. Allo stesso modo, leggere da un database o caricare un’immagine dal disco implica la lettura di un file. Questo genere di cose sono quelle che io chiamo attività di lunga durata – attività che richiedono troppo tempo perché la vostra app si fermi ad aspettarle!

Può essere difficile capire quanto velocemente un telefono moderno esegue il codice rispetto a una richiesta di rete. Su un Pixel 2, un singolo ciclo di CPU richiede poco meno di 0,0000000004 secondi, un numero che è piuttosto difficile da afferrare in termini umani. Tuttavia, se si pensa a una richiesta di rete come a un battito di ciglia, circa 400 millisecondi (0,4 secondi), è più facile capire quanto velocemente opera la CPU. In un battito di ciglia, o in una richiesta di rete un po’ lenta, la CPU può eseguire oltre un miliardo di cicli!

Su Android, ogni app ha un thread principale che si occupa di gestire l’interfaccia utente (come disegnare le viste) e coordinare le interazioni con l’utente. Se c’è troppo lavoro su questo thread, l’app sembra bloccarsi o rallentare, portando a un’esperienza utente indesiderata. Qualsiasi attività di lunga durata dovrebbe essere fatta senza bloccare il thread principale, in modo che la vostra app non mostri ciò che viene chiamato “jank”, come le animazioni congelate, o risponda lentamente agli eventi touch.

Per eseguire una richiesta di rete fuori dal thread principale, un modello comune è quello delle callback. I callback forniscono un handle a una libreria che può essere utilizzato per richiamare il vostro codice in un momento futuro. Con i callback, il recupero di developer.android.com potrebbe apparire così:

class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}

Anche se get è chiamato dal thread principale, userà un altro thread per eseguire la richiesta di rete. Poi, una volta che il risultato è disponibile dalla rete, il callback sarà chiamato sul thread principale. Questo è un ottimo modo per gestire compiti di lunga durata, e librerie come Retrofit possono aiutarvi a fare richieste di rete senza bloccare il thread principale.

Usare le coroutine per compiti di lunga durata

Le coroutine sono un modo per semplificare il codice usato per gestire compiti di lunga durata come fetchDocs. Per esplorare come le coroutine rendono più semplice il codice per i compiti di lunga durata, riscriviamo l’esempio di callback sopra per usare le coroutine.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

Questo codice non blocca il thread principale? Come fa a restituire un risultato da get senza aspettare la richiesta di rete e bloccarsi? Si scopre che le coroutine forniscono un modo per Kotlin di eseguire questo codice senza bloccare il thread principale.

Le coroutine si basano sulle funzioni regolari aggiungendo due nuove operazioni. Oltre a invoke (o call) e return, le coroutine aggiungono suspend e resume.

  • suspend – mette in pausa l’esecuzione della coroutine corrente, salvando tutte le variabili locali
  • resume – continua una coroutine sospesa dal punto in cui è stata messa in pausa

Questa funzionalità è aggiunta da Kotlin tramite la parola chiave suspend sulla funzione. Puoi chiamare le funzioni di sospensione solo da altre funzioni di sospensione, o usando un costruttore di coroutine come launch per iniziare una nuova coroutine.

Sospendi e riprendi lavorano insieme per sostituire le callback.

Nell’esempio sopra, get sospenderà la coroutine prima che inizi la richiesta di rete. La funzione get sarà ancora responsabile dell’esecuzione della richiesta di rete fuori dal thread principale. Poi, quando la richiesta di rete viene completata, invece di chiamare un callback per notificare il thread principale, può semplicemente riprendere la coroutine che ha sospeso.

Animazione che mostra come Kotlin implementa suspend e resume per sostituire le callback.

Guardando come viene eseguita fetchDocs, si può vedere come funziona suspend. Ogni volta che una coroutine viene sospesa, lo stack frame corrente (il posto che Kotlin usa per tenere traccia di quale funzione è in esecuzione e delle sue variabili) viene copiato e salvato per dopo. Quando riprende, lo stack frame viene copiato di nuovo da dove è stato salvato e ricomincia a funzionare. Nel mezzo dell’animazione – quando tutte le coroutine sul thread principale sono sospese, il thread principale è libero di aggiornare lo schermo e gestire gli eventi utente. Insieme, sospendere e riprendere sostituiscono le callback. Piuttosto pulito!

Quando tutte le coroutine sul thread principale sono sospese, il thread principale è libero di fare altro lavoro.

Anche se abbiamo scritto codice sequenziale semplice che assomiglia esattamente a una richiesta di rete bloccante, le coroutine eseguiranno il nostro codice esattamente come vogliamo ed eviteranno di bloccare il thread principale!

Prossimo, diamo un’occhiata a come usare le coroutine per la sicurezza principale ed esploriamo i dispatcher.

Sicurezza principale con le coroutine

Nelle coroutine Kotlin, le funzioni di sospensione ben scritte sono sempre sicure da chiamare dal thread principale. Non importa cosa fanno, dovrebbero sempre permettere a qualsiasi thread di chiamarle.

Ma ci sono un sacco di cose che facciamo nelle nostre applicazioni Android che sono troppo lente per accadere sul thread principale. Richieste di rete, analisi di JSON, lettura o scrittura dal database, o anche solo iterazione su grandi liste. Ognuno di questi ha il potenziale di essere eseguito abbastanza lentamente da causare “jank” visibile all’utente e dovrebbe essere eseguito fuori dal thread principale.

Usare suspend non dice a Kotlin di eseguire una funzione su un thread in background. Vale la pena dire chiaramente e spesso che le coroutine verranno eseguite sul thread principale. Infatti, è davvero una buona idea usare Dispatchers.Main.immediate quando si lancia una coroutine in risposta a un evento UI – in questo modo, se non si finisce per fare un compito di lunga durata che richiede la sicurezza principale, il risultato può essere disponibile nel frame successivo per l’utente.

Le coroutine verranno eseguite sul thread principale, e sospendere non significa background.

Per rendere una funzione che fa un lavoro troppo lento per il thread principale main-safe, puoi dire alle coroutine Kotlin di eseguire il lavoro sul dispatcher Default o IO. In Kotlin, tutte le coroutine devono essere eseguite in un dispatcher – anche quando sono in esecuzione sul thread principale. Le coroutine possono sospendersi da sole, e il dispatcher è la cosa che sa come riprenderle.

Per specificare dove le coroutine devono essere eseguite, Kotlin fornisce tre dispatcher che puoi usare per il dispatch dei thread.

+-----------------------------------+
| Dispatchers.Main |
+-----------------------------------+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.IO |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.Default |
+-----------------------------------+
| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
+-----------------------------------+

* Room fornirà automaticamente la sicurezza principale se usi funzioni di sospensione, RxJava, o LiveData.

** Le librerie di rete come Retrofit e Volley gestiscono i propri thread e non richiedono una main-safety esplicita nel vostro codice quando vengono usate con le coroutine di Kotlin.

Per continuare con l’esempio precedente, usiamo i dispatcher per definire la funzione get. All’interno del corpo di get chiamiamo withContext(Dispatchers.IO) per creare un blocco che verrà eseguito sul dispatcher IO. Qualsiasi codice che mettete dentro quel blocco verrà sempre eseguito sul dispatcher IO. Poiché withContext è essa stessa una funzione di sospensione, funzionerà usando le coroutine per fornire la sicurezza principale.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.Main
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main

Con le coroutine potete fare il dispatch dei thread con un controllo a grana fine. Poiché withContext vi permette di controllare su quale thread ogni linea di codice viene eseguita senza introdurre una callback per restituire il risultato, potete applicarlo a funzioni molto piccole come leggere dal vostro database o eseguire una richiesta di rete. Quindi una buona pratica è usare withContext per assicurarsi che ogni funzione sia sicura per essere chiamata su qualsiasi Dispatcher incluso Main – in questo modo il chiamante non deve mai pensare a quale thread sarà necessario per eseguire la funzione.

In questo esempio, fetchDocs è in esecuzione sul thread principale, ma può tranquillamente chiamare get che esegue una richiesta di rete in background. Poiché le coroutine supportano la sospensione e la ripresa, la coroutina sul thread principale verrà ripresa con il risultato non appena il blocco withContext sarà completo.

Le funzioni di sospensione ben scritte sono sempre sicure da chiamare dal thread principale (o main-safe).

È davvero una buona idea rendere ogni funzione di sospensione main-safe. Se fa qualcosa che tocca il disco, la rete, o anche solo usa troppa CPU, usate withContext per renderla sicura da chiamare dal thread principale. Questo è il modello che le librerie basate sulle coroutine come Retrofit e Room seguono. Se seguite questo stile in tutto il vostro codice, il vostro codice sarà molto più semplice ed eviterà di mescolare i problemi di threading con la logica dell’applicazione. Se seguite in modo coerente, le coroutine sono libere di essere lanciate sul thread principale e di fare richieste di rete o di database con codice semplice, garantendo al contempo che gli utenti non vedranno “jank”.

Le prestazioni di withContext

withContext sono veloci quanto le callback o RxJava per fornire la sicurezza principale. È persino possibile ottimizzare le chiamate withContext oltre a quanto è possibile con le callback in alcune situazioni. Se una funzione farà 10 chiamate a un database, si può dire a Kotlin di passare una volta in un withContext esterno intorno a tutte le 10 chiamate. Quindi, anche se la libreria di database chiamerà withContext ripetutamente, rimarrà sullo stesso dispatcher e seguirà un percorso veloce. Inoltre, il passaggio tra Dispatchers.Default e Dispatchers.IO è ottimizzato per evitare i thread switch quando possibile.

Cosa c’è dopo

In questo post abbiamo esplorato quali problemi le coroutine sono ottime per risolvere. Le coroutine sono un concetto molto vecchio nei linguaggi di programmazione che sono diventati popolari recentemente grazie alla loro capacità di rendere più semplice il codice che interagisce con la rete.

Su Android, è possibile utilizzarle per risolvere due problemi molto comuni:

  1. Semplificare il codice per compiti di lunga durata come la lettura dalla rete, dal disco, o anche l’analisi di un grande risultato JSON.
  2. Facendo una precisa main-safety per assicurarsi di non bloccare mai accidentalmente il thread principale senza rendere il codice difficile da leggere e scrivere.

Nel prossimo post esploreremo come si inseriscono su Android per tenere traccia di tutto il lavoro iniziato da uno schermo! Dategli una letta:

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.