Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Il pattern async/await

Link copiato negli appunti

Prima dell'arrivo di Windows 8, la gran parte delle API native erano esposte mediante metodi sincroni. Ad esempio, in .NET per leggere il contenuto di un file di testo avremmo potuto usare il seguente codice:

String content;
using (var sr = new StreamReader("document.txt"))
{
    content = sr.ReadToEnd();
}

In questo caso, la chiamata al metodo ReadToEnd è sincrona, il che significa che il controllo non viene restituito al chiamante fino a quando le operazioni di accesso e lettura del file non sono completate. Se, ad esempio, questo codice fosse collegato al click di un pulsante o ad un'altra operazione eseguita sulla UI, la user interface rimarrebbe "congelata" per tutto il tempo necessario a completare la lettura del file.

Una situazione analoga si presenta per tutti quei metodi che hanno 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, ecc. Questo problema può essere risolto eseguendo l'operazione "lenta" in modalità asincrona, ossia restituendo immediatamente il controllo al chiamante (alla UI, in questo esempio) e continuando le relative operazioni su un thread secondario. In questo modo, il chiamante di una funzione non ha bisogno di aspettare il completamento di un'operazione prima di procedere oltre, ricevendo comunque il relativo risultato al termine della stessa.

Poiché fino alla versione 4.0, il .NET Framework non esponeva una versione asincrona del metodo ReadToEnd, se volevamo evitare il congelamento della UI la relativa chiamata doveva essere inserita in un thread separato.

Ad esempio, il codice seguente definisce un thread (codice .NET 1.0) e il relativo metodo (FaiQualcosa) che verrà richiamato alla partenza del thread.

Thread wkThread = new Thread(new ThreadStart(FaiQualcosa));
wkThread.Start();

Oltre a definire un oggetto di classe Thread e avviarlo tramite il metodo Start, sin dalla versione 1 del .NET Framework è possibile definire un delegate e invocarlo in asincrono.

private delegate void NoParamDelegate();
NoParamDelegate npd = new NoParamDelegate(FaiQualcosa);
npd.Invoke();

Dal Framework versione 4.0 è possibile semplificare la gestione dei Thread (in molti casi) tramite l'utilizzo della Task Parallel Library. Ad esempio il codice seguente avvia la lettura dello stream all'interno di un nuovo task:

Task.Factory.StartNew(() =>
{
    using (StreamReader sr = new StreamReader("document.txt"))
    {
        return sr.ReadToEnd();
    }
}).ContinueWith(
    (t) => t.Result
    // aggiornare UI
);

La versione 4.5 del framework ha modificato la situazione, introducendo una versione asincrona per tutti quei metodi che possono impiegare "del tempo" per poter essere eseguiti. Seguendo questa evoluzione, WinRT espone una versione asincrona per quei metodi il cui tempo di risposta possa risultare superiore a 50 millisecondi, in modo da assicurare la fluidità della user experience. (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).

Contestualmente, con la versione 5 di C# sono state introdotto due nuove parole chiave, async e await, che rendono la scrittura di codice asincrono più facile e "naturale" (perché più simile al più familiare codice sincrono).

Il prossimo snippet mostra una nuova versione del codice sopra illustrato, adattato per rispondere al nuovo pattern async/await:

public async void UpdateUIFromFile()
{
    String content;
    using (StreamReader sr = new StreamReader("document.txt"))
    {
        content = await sr.ReadToEndAsync();
        // aggiornare UI
    }
}

Qui di seguito, invece, trovate un esempio di una delle numerose API asincrone esposte da WinRT (abbiamo già usato un codice simile nell'articolo dedicato alla classe CameraCaptureUI):

private async void CapturePhoto_Click(object sender, RoutedEventArgs e)
{
    var camera = new CameraCaptureUI();
    StorageFile img = await camera.CaptureFileAsync(CameraCaptureUIMode.Photo);
    if (img != null)
    {
	using (var stream = await img.OpenAsync(FileAccessMode.Read))
        {
            var bitmap = new BitmapImage();
            bitmap.SetSource(stream);
            takenImage.Source = bitmap;
        }
    }
    else
    {
	// no foto
    }
}

Subito dopo aver creato un'istanza della classe CameraCaptureUI, il codice invoca il metodo CaptureFileAsync per catturare un'immagine dalla webcam, facendo precedere la chiamata al metodo asincrono dalla parola chiave await. Questa espressione fa sì che il blocco di codice successivo all'istruzione stessa venga eseguito in un task separato. In questo modo, il controllo può essere immediatamente restituito al chiamante (in questo caso la message pump di Windows), che può così continuare con altre operazioni che non dipendono dal risultato finale di quella asincrona, senza "congelarsi".

Quando l'operazione asincrona viene completata, il metodo (CapturePhoto_Click, in questo caso) riprenderà l'esecuzione dall'istruzione successiva (più precisamente, dall'istruzione if). Questo spiega come mai Il valore restituito dal metodo CaptureFileAsync sia di tipo Task<StorageFile>. Tuttavia, grazie alla sostituzione operata per noi dal compilatore in presenza di un'istruzione await, nel nostro codice possiamo usare il valore restituito dal Task direttamente come classe Windows.Storage.StorageFile. Infatti, tutto il codice necessario a recuperare il valore dalla proprietà Result dell'istanza di tipo Task viene generato automaticamente dal compilatore grazie all'uso delle parole chiave await e async.

In particolare, quando un metodo viene marcato come async, questo viene riscritto dal compilatore in modo tale da permettere la sospensione della sua esecuzione (il che avviene, come si è detto, in corrispondenza della parola chiave await) e la sua successiva ripresa, una volta che l'operazione asincrona si è conclusa, nello stesso stato lasciato al momento della sospensione (come ad esempio, il valore delle variabili di metodo).

Per verificare con mano quando detto, è sufficiente dare un'occhiata all'IL (Intermediate Language) prodotto dal compilatore in presenza di un metodo asincrono, come illustrato nella prossima immagine:

Implementare un metodo asincrono

È importante precisare che l'uso della parola chiave async non trasforma, di per sé, un metodo sincrono in asincrono. Essa permette di abilitare l'uso dell'istruzione await all'interno di un metodo, in modo da poter sospendere l'esecuzione del codice, restituendo il controllo al chiamante, eseguire l'operazione asincrona in un thread separato e gestire, ove presente, il valore di ritorno dell'operazione.

Per comprendere meglio questo punto, proviamo a implementare una semplicissima applicazione Windows Store il cui unico compito è quello di leggere il contenuto di un file di testo selezionato dall'utente e mostrarlo a schermo:

private async void ChooseFile_Click(object sender, RoutedEventArgs e)
{
    this.LongOperation(5);
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    picker.FileTypeFilter.Add(".txt");
    var file = await picker.PickSingleFileAsync();
    string content = await Windows.Storage.FileIO.ReadTextAsync(file);
    this.FileContentTextBlock.Text = content;
}
private void LongOperation(Int32 seconds)
{
    DateTime exitTime = DateTime.Now.AddSeconds(seconds);
    while (DateTime.Now < exitTime) ;
}

In questo esempio, il metodo LongOperation si limita a tenere impegnato il thread per un tempo pari al numero di secondi ricevuto come parametro. Il codice dell'handler dell'evento di click (contrassegnato dalla parola chiave async) viene chiamato in modo sincrono dalla message pump di Windows, quindi prosegue sempre in sincrono fino a quando non viene trovata la parola chiave await. Questo significa che, per cinque secondi, l'applicazione risulterà "congelata", come potete facilmente sperimentare eseguendo l'applicazione e premendo il pulsante.

Per testare questo codice, potete usare la seguente definizione XML come riferimento per la vostra MainPage.xaml.

<Page
    x:Class="Demo.Html.it.AsyncProgramming.CS.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Demo.Html.it.AsyncProgramming.CS"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel>
            <Button Click="ChooseFile_Click" Margin="20">Scegli un file</Button>
            <TextBlock x:Name="FileContentTextBlock" Width="Auto" Margin="20" FontSize="20"></TextBlock>
        </StackPanel>
    </Grid>
</Page>

Un modo per ovviare a questo problema è quello di trasformare il metodo LongOperation in un'operazione asincrona mediante l'uso delle parole chiave async e await, come illustrato nel prossimo snippet:

private async void ChooseFile_Click(object sender, RoutedEventArgs e)
{
    await this.LongOperationAsync(5);  // aggiunto await
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    picker.FileTypeFilter.Add(".txt");
    var file = await picker.PickSingleFileAsync();
    string content = await Windows.Storage.FileIO.ReadTextAsync(file);
    this.FileContentTextBlock.Text = content;
}
// aggiunto async e modificato il nome del metodo
private async Task LongOperationAsync(Int32 seconds)
{
    // aggiunto task asincrono
    await Task.Factory.StartNew(() =>
    {
        DateTime exitTime = DateTime.Now.AddSeconds(seconds);
        while (DateTime.Now < exitTime) ;
    });
}

Se ora eseguite l'applicazione, noterete che alla pressione del pulsante la user interface non si bloccherà (come potete notare dallo stato del pulsante). Al click del mouse, infatti, la prima delle tre operazioni asincrone (LongOperation) sarà eseguita in un thread secondario, mentre il resto del codice verrà eseguito al termine dell'operazione, ossia trascorsi cinque secondi dal click. A questo punto, il codice, dopo aver istanziato un oggetto di tipo FileOpenPicker, attenderà nuovamente che l'utente selezioni un file sul disco (tramite il metodo PickSingleFileAsync) e, terminata anche questa operazione, provvederà a leggere il contenuto del file (ReadTextAsync). Solo quando anche quest'ultima operazione asincrona sarà terminata, il contenuto del file verrà mostrato a schermo.

È importante osservare come il metodo LongOperationAsync sia passato da void a restituire un oggetto di tipo Task. Perché infatti un metodo possa essere "awaitable", occorre infatti che restituisca un Task. In particolare:

  • Se il metodo è void, il metodo asincrono deve restituire un Task.
  • Se il metodo restituisce un tipo T, il metodo asincrono deve restituire un Task<T> (in base a questa regola, ad esempio, un metodo che restituisce un Int32, nella sua versione asincrona deve restituire un Task<Int32>).

La ragione per cui l'handler dell'evento di click restituisce void, anziché Task, sta nel fatto che non è invocato tramite un await; al contrario, viene invocato secondo l'approccio "fire-and-forget". Si tratta di un approccio che sarebbe opportuno non adottare per il proprio codice, dal momento che, come vedremo meglio nel prossimo articolo, le eccezioni sollevate durante l'operazione asincrona non possono essere intercettate da un eventuale blocco try/catch posto intorno alla chiamata al metodo asincrono.

Tornando al codice visto in precedenza, abbiamo notato come, nonostante la UI non risulti bloccata, la user interface del picker venga mostrata solo dopo cinque secondi dal click iniziale. Questo accade perché, come si è detto, l'esecuzione dell'handler viene sospesa già al primo await, riprendendo solo al compimento dell'operazione stessa.

Per svolgere un'operazione di lunga durata, senza costringere l'utente ad attendere passivamente la conclusione di questa, possiamo manipolare direttamente l'oggetto Task restituito dalla chiamata al metodo LongOperationAsync, come mostrato nel prossimo snippet:

private async void ChooseFile_Click(object sender, RoutedEventArgs e)
{
    var longOp = this.LongOperationAsync(10);
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    picker.FileTypeFilter.Add(".txt");
    var file = await picker.PickSingleFileAsync();
    string content = await Windows.Storage.FileIO.ReadTextAsync(file);
    await longOp;
    this.FileContentTextBlock.Text = content;
}

In questo codice abbiamo spostato l'istruzione await in fondo al metodo, giusto prima di visualizzare il contenuto del file a schermo. In questo modo l'esecuzione delle due operazioni, quella rappresentata dal metodo LongOperationAsync e la lettura del file di testo, sarà contemporanea, mentre l'aggiornamento della UI avverrà comunque trascorsi dieci secondi dal click del mouse.

Quanto al metodo LongOperationAsync, tenete comunque presente che la creazione di un nuovo Task potrebbe condurre ad eseguire il codice in un thread differente, con conseguenti problemi di "race condition" (ossia di accesso concorrente per cui la stessa variabile, o la stessa struttura in memoria, viene letta e scritta contemporaneamente da thread diversi).

Nel prossimo articolo approfondiremo ulteriormente questi concetti.


Ti consigliamo anche