Prima dell'arrivo di Windows 8, la gran parte delle API erano esposte mediante metodi sincroni. Questo valeva anche per metodi con un tempo di risposta potenzialmente lungo, come la lettura e la scrittura file, l'accesso al web, la cattura e la riproduzione di file multimediali, etc.
Nel caso di chiamate a metodi sincroni il controllo non viene restituito al chiamante fino a quando le relative operazioni (ad esempio, aprire e leggere un file, scaricare un file dal web, ecc.) non siano state completate. Qualora la chiamata a un metodo sincrono è collegata al click di un pulsante o ad un'altra operazione eseguita sulla UI, la user interface rimane dunque "congelata" per tutto il tempo necessario a completare l'operazione.
Con la versione 4.5 del framework la situazione cambia radicalmente, grazie all'introduzione delle versioni asincrone per tutti i metodi con un tempo potenziale di risposta troppo lungo.
Per assicurare fluidità alla user experience, WinRT arriva ad esporre la sola versione asincrona per quei metodi il cui tempo di risposta possa risultare superiore a 50 millisecondi.
Ad esempio, qualunque API che includa, esplicitamente o implicitamente, operazioni di I/O rientra in quest'ultima categoria, poiché il tempo di risposta per questo tipo di operazioni non è sempre prevedibile a priori.
Pattern asicnrono e linguaggi di programmazione
Tutti i linguaggi che consentono di realizzare applicaizoni Windows Store (JavaScript, C#, Visual Basic, e C++) supportano, seppure con modalità differenti, l'uso di pattern di programmazione asincrona.
In C# e VB è stato introdotto un nuovo pattern, basato sulle parole chiave async/await, che rende l'utilizzo delle nuove API asincrone di Windows 8 semplice e naturale.
L'istruzione await, in particolare, permette di effettuare una chiamata a un'API asincrona, sospendere l'esecuzione del blocco di codice successivo all'istruzione await
fino al completamento dell'operazione, e quindi riprendere l'esecuzione del codice dal punto in cui era stata sospesa.
In C++, la programmazione asincrona si basa sulla classe task e sul metodo then, secondo un modello di programmazione non troppo diverso rispetto a quello previsto per le app Windows Store in JavaScript che descriviamo ora.
L'uso di promise per l'esecuzione di codice asincrono
A differenza degli altri linguaggi supportati (C#, VB e C++), JavaScript è un linguaggio single-threaded, quindi basato su un singolo thread di esecuzione. Questo significherebbe dover aspettare sempre il termine di un'operazione prima di passare alla successiva, quindi nel caso di operazioni di lunga durata, veremmo spesso la UI "congelata" nell'attesa.
Per aggirare questo problema, una delle soluzioni più diffuse è quella di passare una callback alla funzione in questione e, anziché aspettare il compimento dell'operazione, restituire il controllo al chiamante. Quando la funzione avrà terminato le sue operazioni, la funzione di callback verrà invocata, eventualmente passando come parametro il risultato finale dell'operazione.
L'uso di callback, spesso nidificate l'una dentro l'altra, rende tuttavia ardua la leggibilità del codice e le operazioni di debug. Per semplificare il lavoro degli sviluppatori, WinRT e la Windows Library for JavaScript (WinJS) hanno scelto un approccio diverso: tutte le API asincrone esposte da WinRT (che includono, come si è detto, tutte quelle API il cui tempo di risposta potrebbe superare i 50 millisecondi), quando chiamate da codice JavaScript, restituiscono delle promise (così come definite dalla Common JS Promises/A, una proposta per il supporto delle promise).
Come il nome suggerisce, una promise può essere definita come la "promessa" di restituire un certo valore in un qualche momento futuro. In realtà, una promise non è altro che un oggetto JavaScript, ed è perfino possibile studiare la definizione del relativo tipo, WinJS.Promise, all'interno del file base.js
presente di default nelle applicazioni Windows Store in HTML5/JavaScript.
In quanto oggetti, tutte le promise espongono gli stessi metodi, come then e done.
then
Il metodo then rappresenta, per restare in tema di promesse, l'accettazione da parte del client della promessa di restituire un determinato valore al termine dell'operazione da parte di "colui che promette" (detto anche operator).
Questa funzione accetta tre parametri:
- un handler per il completamento dell'operazione che verrà invocato (in modo sincrono) quando il valore promesso sarà divenuto disponibile;
- un handler (opzionale) per gestire il caso in cui non ci sia nessun valore, perché si è verificato un errore durante l'esecuzione dell'operazione;
- un handler (anch'esso opzionale) da chiamare periodicamente per recuperare i valori intermedi, in modo da essere aggiornati sullo stato di avanzamento dell'operazione (nel caso in cui l'API supporti questa possibilità).
Come tutte le promesse, infatti, anche una promise JavaScript può non essere "mantenuta". In particolare, una promise può assumere uno dei seguenti stati (rappresentati dall'enum Windows.Foundation.AsyncStatus
, accessibile tramite la proprietà promise.operation.status
):
Proprietà | Descrizione |
---|---|
Canceled | L'operazione è stata annullata. |
Completed | L'operazione è stata completata. |
Error | Si è verificato un errore durante l'operazione. |
Started | L'operazione è stata avviata. |
È importante ricordare che la funzione then restituisce sempre una nuova promise. In questo modo, è possibile concatenare successive chiamate a questa funzione, come mostrato nel seguente scheletro di codice:
operation1().then(function (result1) {
return operation2(result1);
}).then(function (result2) {
return operation3(result2);
}).then(function (result3) {
// etc.
});
La funzione operation2
sarà invocata solo dopo che operation1
è stata completata e riceverà come parametro il risultato della prima operazione, ossia result1
. Analogamente, la funzione operation3
verrà eseguita solo dopo il completamento della precedente, ricevendo come parametro result2
; e così via.
Il seguente snippet mostra un esempio di utilizzo di concatenazione di più promise per compiere più operazioni, una di seguito all'altra:
function updateUIfromFile_click() {
var picker = new Windows.Storage.Pickers.FileOpenPicker();
picker.fileTypeFilter.replaceAll([".txt"]);
picker.pickSingleFileAsync()
.then(function (file) {
return Windows.Storage.FileIO.readTextAsync(file);
}, errorHandler, null)
.then(function (content) {
messageText.innerText = content;
}, errorHandler, null);
}
function errorHandler(err) {
// notificare utente
}
Dopo aver creato un nuovo oggetto di tipo FileOpenPicker
, il codice chiama il metodo asincrono PickSingleFileAsync
. Dal momento che questo metodo (così come tutte le API asincrone di WinRT, come si è detto) restituisce una promise, tramite la prima chiamata alla funzione then
subordiniamo l'esecuzione del blocco di codice successivo (ossia quello che effettua la lettura del contenuto del file di testo) al completamento della prima operazione:
.then(function (file) {
return Windows.Storage.FileIO.readTextAsync(file);
}, errorHandler, null)
Per far questo, passiamo alla funzione then
, come primo parametro, la funzione che dovrà essere eseguita solo dopo che l'utente ha selezionato un file tramite il picker (ossia quando il valore di ritorno, rappresentato da un oggetto di tipo StorageFile
, sarà disponibile). Il secondo parametro è rappresentato, come si è detto, dalla funzione che sarà chiamata qualora, durante l'esecuzione dell'operazione asincrona, sia stato sollevato un errore. Il terzo parametro rappresenta infine l'handler relativo al progredire dell'operazione (in questo caso null
).
.then(function (content) {
messageText.innerText = content;
}, errorHandler, null);
Dal momento che l'handler di completamento contiene un'ulteriore chiamata a un'API asincrona di WinRT, che a sua volta restituisce un'altra promise, possiamo concatenare una seconda chiamata alla funzione then
, subordinando l'aggiornamento della UI al completamento delle operazioni di lettura del file.
Nidificare le promise
Anziché concatenare le promise, avremmo potuto anche nidificarle in questo modo (notate l'assenza dell'istruzione return
prima della chiamata al metodo ReadTextAsync
):
picker.pickSingleFileAsync()
.then(function (file) {
Windows.Storage.FileIO.readTextAsync(file)
.then(function (content) {
messageText.innerText = content;
}, errorHandler, null)
}, errorHandler, null);
Come vedremo meglio nel prossimo articolo, però, si tratta di una pratica da sconsigliare, poiché rende il codice difficoltoso da leggere e da testare.
done
Accanto a then
, il tipo Promise
espone anche una seconda funzione, done, che pur accettando gli stessi parametri della prima, non restituisce però una nuova promise. Per questa ragione, la funzione done
dovrebbe essere l'ultima ad essere chiamata in una catena di promise.
Inoltre, qualora si verifichi un errore durante l'operazione asincrona, done
solleverà sempre un'eccezione, a meno che non sia stato espressamente passato un error handler. Il prossimo snippet mostra una catena di promise chiuse da una chiamata alla funzione done
.
var picker = new Windows.Storage.Pickers.FileOpenPicker();
picker.fileTypeFilter.replaceAll([".txt"]);
picker.pickSingleFileAsync()
.then(function (file) {
return Windows.Storage.FileIO.readTextAsync(file);
}, errorHandler, null)
.done(function (content) {
messageText.innerText = content;
}, errorHandler, null);
Creare una nuova promise
Il modo più semplice di creare una promise è mostrato nel prossimo snippet:
function longOperation(seconds) {
return new WinJS.Promise(function (completeHandler, errorHandler, progressHandler) {
try {
var result = 0;
// operazione lunga
// imposta la variabile result
// invoca la funzione ricevuta dalla promise
// come completed handler, passando result come valore di ritorno
completeHandler(result);
} catch (e) {
errorHandler(e);
}
});
}
Come si può notare, il costruttore dell'oggetto WinJS.Promise accetta come parametro una funzione che incapsula la logica dell'operazione da compiere. Al termine dell'operazione, viene invocata la funzione passata alla promise come "completed handler" (o, in caso di errore, la funzione passata come "error handler"), passando come parametro il valore da restituire.
È molto importante ricordare che la funzione viene eseguita sempre sul thread corrente (come abbiamo già ricordato, infatti, JavaScript è un linguaggio single-threaded). Questo significa che non è sufficiente incapsulare del codice all'interno di una promise per trasformarlo automaticamente un metodo asincrono. Prendiamo per esempio la seguente funzione:
function updateUIfromFile_click() {
longOperation(10000000);
(codice omesso)
}
function longOperation(max) {
return new WinJS.Promise(function (completeHandler, errorHandler) {
var result = 0;
function justWait() {
try {
for (var i = 0; i < max; i++) {
result += i;
}
completeHandler(result);
} catch (e) {
errorHandler(e)
}
}
justWait()
});
}
In questo esempio, la funzione longOperation
si limita a tenere impegnato il thread per un tempo pari al numero di secondi ricevuto come parametro. L'handler dell'evento di click viene chiamato in modo sincrono dall'Event Loop di JavaScript. Questo significa che, fino al completamento dell'operazione, l'applicazione risulterà "congelata". Una volta conclusa l'operazione, sarà invocato il completed handler (passando il risultato finale come parametro) e l'interfaccia utente del file picker apparirà, permettendo di selezionare il file.
Per testare questo codice, utilizziamo il seguente markup HTML per la pagina di default dell'app.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Demo.Html.it.AsyncProgramming.JS</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.2.0/js/base.js"></script>
<script src="//Microsoft.WinJS.2.0/js/ui.js"></script>
<!-- Demo.Html.it.AsyncProgramming.JS references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
</head>
<body>
<button id="btnChooseFile">Scegli file</button>
<div id="messageText"></div>
</body>
</html>
Se vogliamo trasformare questa chiamata da sincrona a asincrona, è necessario modificare il codice della funzione, ad esempio suddividendo l'esecuzione dell'operazione in più "tranche" da eseguirsi una dietro all'altra. Ecco come potrebbe essere riscritta la funzione:
function updateUIfromFile_click() {
longOperation(10000000, 10)
.then(function (result) {
messageText.innerText = result;
});
(codice omesso)
}
function longOperation(max, step) {
return new WinJS.Promise(function (completeHandler, errorHandler, progressHandler) {
var result = 0;
function justWait(args) {
try {
for (var i = args.start; i < args.end; i++) {
result += i;
};
if (i >= max) {
completeHandler(result);
} else {
setImmediate(justWait,
{ start: args.end, end: Math.min(args.end + step, max) });
}
} catch (e) {
errorHandler(e);
}
}
setImmediate(justWait, { start: 0, end: Math.min(step, max) });
});
}
La funzione setImmediate
(questa funzione è stata introdotta da Microsoft con Internet Explorer 10, il cui engine è stato usato come ambiente di esecuzione del codice Javascript in WinRT) permette di aggiungere il task ricevuto come parametro (justWait
, nell'esempio) alla coda di esecuzione affinché venga eseguito non appena la coda è libera. Se adesso eseguite questo codice e cliccate sul pulsante, vedrete che la user interface del file picker apparirà immediatamente, mentre il risultato della promise verrà visualizzato solo dopo diversi secondi (per cui, a meno che non abbiate impiegato troppo tempo a selezionare il file di testo, apparirà dopo che il contenuto del file è stato letto.
Nel prossimo articolo vedremo ulteriori dettagli su questi aspetti.