Programmazione Asincrona

P

Cos’è la programmazione asincrona? E perché è importante?

Sono pronto a fare una scommessa (ma per sicurezza eviterò, si sa mai!): la maggioranza di voi ha usato almeno una volta nella sua vita, cosciente o meno del fatto, la programmazione asincrona.
In effetti la programmazione asincrona esiste sin dall’alba dei tempi, ma perché?
Ebbene, pensate a un caso semplice: una comunissima UI. Che sia da linea di comando o con interfaccia grafica, poco importa; tuttavia un esempio basato su interfaccia grafica rende molto meglio l’idea. Avete avuto l’idea del secolo, con un’interfaccia grafica cazzutissima, ma avete un problema: quando l’utente preme un bottone, questo bloccherà tutta l’interfaccia grafica finché non ha finito di eseguire i suoi compiti.
Potete immaginarvi una situazione del genere? Io sì e, detto francamente, odierei a morte la cosa.
Proprio in questo contesto entra in gioco la programmazione asincrona: mentre il bottone fa le sue cose, l’utente può continuare a premere altri fantastici bottoni, poi quando il bottone avrà finito, allora il risultato dell’operazione sarà disponibile all’utente.
Volendo essere un po’ più formali, si può dire che:

La programmazione asincrona è una forma di programmazione parallela che permette ad un’unità di lavoro di funzionare separatamente dal thread principale, notificandogli quando avrà finito il lavoro.

Eventi e Callback

Perfetto: sappiamo che dobbiamo usare la programmazione asincrona per evitare che si blocchi l’interfaccia alla pressione di un bottone, ma come facciamo a dirgli che quando viene premuto, deve fare qualcosa?
La risposta sta proprio nella parola quando, ovvero nella gestione degli eventi.
Immaginate il seguente pseudo-codice:

bottoneBellissimo.onPress(faiCoseFantastiche());

Quel simpatico onPress è un modo intelligente del vostro linguaggio di programmazione preferito per dire “quando premi il bottone, crea un altro thread che esegua la funzione faiCoseFantastiche”.
Ottimo! Ora il nostro utente potrà premere i bottoni senza che si blocchi tutto e questi bottoni, incredibilmente, faranno quello che devono.
Un momento però: perché l’utente continua a premere sempre lo stesso bottone che fa sempre la stessa cosa? Magari facendogli comparire un messaggio che gli comunichi che il bottone ha finito, smetterebbe di cliccarlo in modo forsennato. Ma come?
La risposta al quesito sono le famigerate callback, ovvero passare delle funzioni all’evento, che verranno eseguite quando questo ha completato il suo lavoro.
Un classico esempio potrebbe essere il seguente:

bottoneBellissimo.onPress(faiCoseFantastiche(mostraIlMessaggio));

Tale riga è da interpretarsi come: quando viene premuto il bottone, faiCoseFantastiche; quando hai finito, mostraIlMessaggio.
Ovviamente un qualsiasi linguaggio decente, permetta di fornire più callback a seconda del responso dell’evento: successo o fallimento.

Promises

A questo punto sappiamo come fare una chiamata asincrona e come gestirne il responso, cambiando il comportamento in caso di successo o fallimento.
Ora però immaginiamo una callback che al suo interno fa una nuova chiamata asincrona, la cui callback fa una nuova chiamata asincrona e così via a piacere.
Pensate di essere in grado di gestire e mantenere un codice il cui flusso è così spezzettato senza problemi? Se la risposta è sì, probabilmente la vostra sanità mentale vi ha già abbandonato da un po’ e siete pronti per programmare un microprocessore scrivendo direttamente in esadecimale. Se al contrario l’idea vi ha fatto salire un discreto mal di testa, starete ormai pensando “ti prego, dicci che c’è una soluzione a ciò!”
Tranquilli, la soluzione c’è e si chiama promises. In realtà promises è il nome che dà JavaScript al concetto; altri linguaggi lo chiamano in altro modo, ma il succo è sostanzialmente lo stesso.
L’idea alla base è questa: quando faccio una chiamata asincrona, perché invece di dovergli passare delle callback non mi ritorna un oggetto che posso gestire come voglio? Ebbene tale oggetto si chiama promise ed espone tre stati:

  • In attesa (pending): l’operazione asincrona è in esecuzione
  • Soddisfatta (fulfilled): l’operazione asincrona è completata con successo e il suo risultato è pronto all’uso
  • Respinta (rejected): l’operazione asincrona è fallita

È bene notare che le uniche transazioni possibili sono:

  • In attesa → Soddisfatta
  • In attesa → Respinta

Vien da sé che quanto una promise sarà completata o rifiutata, resterà tale per sempre.
Ora il quesito è: va bene, abbiamo la promise, come la usiamo?
Niente di più semplice! Vi basterà usare i metodi then per gestire il successo e catch per gestire il fallimento:

faiCoseFantastiche() {
  funzioneAsincrona()
    .then(gestisciSuccesso())
    .catch(gestisciFallimento());
}

In questo piccolo esempio non sembra uno strumento chissà quanto potente, ma il vero vantaggio delle promises si presenta, appunto, con chiamate asincrone annidate.
Supponiamo di nuovo di avere la situazione indecente di chiamate asincrone detta poco fa.
In questo caso, invece di dover gestire le callback a cascata, avremmo delle promises che rendono la questione leggibile:

faiCoseFantastiche() {
	funzioneAsincronaAnnidata()
		.then(annidata1())
		.then(annidata2())
		.then(gestisciSuccesso());
}

In poche parole, il metodo then ritorna una nuova promise inerente alla funzione asincrona che gli è stata passata. La cascata di then invece che callback annidate vi permette di seguire meglio il flusso, non trovate?

Async e Await

Dopo un po’ che programmate in modo asincrono tramite promises, avrete come l’impressione programmare in modo sincrono, ma in modo un piuttosto contorto.
Per ovviare al problema, sono stati introdotti i concetti di async e await.
In poche parole, grazie a queste due parole chiave, non sarete voi a gestire le chiamate asincrone ma sarà il compilatore a farlo per voi.
Il nostro codice, diventerà allora qualcosa del genere:

async faiCoseFantastiche() {
	return await funzioneAsincrona();
}

In poche parole, async indica al compilatore che la funzione corrente, da qualche parte, farà una chiamata asincrona, e che quindi deve generare un nuovo thread per la stessa. Il significato di await invece è ancora più chiaro in quanto dice semplicemente al compilatore di non andare oltre fintanto che la funzione asincrona non è completata.
Una volta scritta la vostra funzione tramite async e await, non dovete fare altro che utilizzarla così come la utilizzereste se fosse sincrona.

Conclusioni

Tutti gli ingredienti per poter maneggiare le funzioni asincrone sono ora a vostra disposizione, non resta che scoprire quali caratteristiche supporti il linguaggio di programmazione che dovete e/o volete usare e imparare di conseguenza la sintassi corretta.

Una precisazione finale è doverosa: durante tutto l’articolo ho parlato di programmazione asincrona relativa alla GUI in quanto è il contesto in cui l’utente più tende a mal sopportare le latenze, ma non è di certo l’unico caso in cui la programmazione asincrona svolge un compito cruciale.

In particolare possiamo suddividere le operazioni svolte dal computer in due macro-categorie: operazioni legate prevalentemente al calcolo e operazioni legate all’I/O.

Le prime sono tutte quelle che fanno uso intensivo della CPU, ovvero tutte quelle operazioni che fanno un qualche calcolo matematico estremamente complesso ma confinato a sé stesso. Ad esempio un programma di elaborazione dell’immagine, una volta caricata la stessa, si baserà solo su conti matematici per ogni qualsivoglia manipolazione sia necessaria.

Le seconde invece, usano poco o nulla la CPU. Il loro processo vitale si limita ad inviare richieste al dispositivo di I/O ed aspettare che questo finisca il suo lavoro. Esempi classici sono un qualsiasi gestore di file, un DBMS o un qualsiasi software che accede alla rete: un browser ad esempio.

Ogni sistema operativo permette allo sviluppatore di utilizzare i dispositivi di I/O sia in modo sincrono che asincrono. Ciò è dovuto al fatto che, se il vostro programma usa un dispositivo di I/O in modo sincrono, resterà bloccato fintanto che questo non avrà finito. Volendo a tutti i costi usare la programmazione sincrona, si potrebbe decidere di creare un nuovo thread a mano che faccia la richiesta al dispositivo, mentre il vostro programma continua nel suo flusso di lavoro.

Leggendo l’articolo avrete ormai capito che la programmazione asincrona, in sottofondo, fa proprio questa operazione; nascondendovi però i dettagli implementativi della creazione del thread, della sincronizzazione (e quindi le varie race-condition che possono sorgere).

In sostanza, che abbiate una macchina a singolo core o un computer che può farvi anche il caffè, comunque usare la programmazione asincrona per gestire l’I/O, ne aumenterà sensibilmente la scalabilità, riducendo la complessità del codice che sarebbe necessario per gestire più thread.

Per il resto, l’unico limite è la vostra fantasia, solo sperimentando saprete se una soluzione asincrona è meglio di una sincrona per risolvere un dato problema.

Sitografia

I Programmer – What Is Asynchronous Programming?

A proposito di me

Nico Caprioli

Si diletta con i computer sin dall'età di 8 anni, per sbarcare nel mondo della programmazione durante il liceo.
Dopo una laurea magistrale in Ingegneria Informatica, passa le sue giornate a battere i tasti sperando di scrivere codice C# valido.

Di Nico Caprioli

Gli articoli più letti

Articoli recenti

Commenti recenti