Nell'articolo precedente abbiamo accennato al fatto che le API asincrone di WinRT lavorano, "under the hood", con degli oggetti Task
. È tempo di approfondire meglio questo punto. In realtà, se si osserva il tipo restituito dai metodi asincroni esposti da WinRT, si vedrà che questi metodi, anziché restituire le tradizionali istanze di Task
e Task<TResult>
tipiche dei metodi asincroni in .NET 4.5, restituiscono oggetti che implementano particolari interfacce (definiti anche "future type").
In particolare, le API asincrone di WinRT supportano internamente quattro diversi tipi di interfaccia, a seconda che restituiscano o meno un valore, e che supportino o meno l'aggiornamento sullo stato di avanzamento delle relative operazioni. Tutte queste interfacce derivano da una medesima interfaccia base, IAsyncInfo
, la quale espone un set di funzionalità comuni ai diversi future type. Questa è la relativa definizione:
public interface IAsyncInfo
{
AsyncStatus Status { get; }
Exception ErrorCode { get; }
uint Id { get; }
void Cancel();
void Close();
}
Queste quattro interfacce sono le seguenti:
Interfaccia | Descrizione |
---|---|
IAsyncAction | rappresenta un'azione asincrona che non prevede un valore di ritorno e non supporta la notifica dello stato di avanzamento dell'operazione (in questa interfaccia il metodo GetResult serve unicamente a sollevare un'eccezione nel caso in cui si sia verificato un errore durante l'operazione asincrona; in questo caso, infatti, GetResult è un metodo void ). |
IAsyncActionWithProgress<TProgress> | rappresenta un'azione asincrona che non prevede un valore di ritorno, ma che supporta la notifica dello stato di avanzamento (valgono qui gli stessi rilievi per il metodo GetResult ). |
IAsyncOperation<TResult> | rappresenta un'operazione asincrona che restituisce un valore di tipo TResult , ma non supporta la notifica dello stato di avanzamento. |
IAsyncOperationWithProgress<TResult, TProgress> | rappresenta un'operazione asincrona che restituisce un valore di tipo TResult e che supporta la notifica dello stato di avanzamento. |
La scelta di usare future type in WinRT in luogo della classica Task Parallel Library è dovuta al fatto che la classe Task
è, in realtà, un tipo del Common Language Runtime ed è dunque disponibile solo per quei linguaggi basati sul runtime di .NET (come C# e VB). Le API di WinRT, invece, devono poter essere accessibili anche a linguaggi come JavaScript e C++. Grazie all'uso di queste interfacce, WinRT può così introdurre un ulteriore livello di astrazione nel modello di programmazione asincrona, lasciando a ciascun linguaggio il compito di definirne la relativa implementazione.
Nel caso di linguaggi CLR, in particolare, questa implementazione poggia sulla Task Parallel Library, come è facilmente verificabile andando a ispezionare il codice IL prodotto dal compilatore. Infatti, quando una chiamata alle API asincrone di WinRT viene preceduta da un'istruzione await, ad essere invocato è il metodo Task.GetAwaiter
(), un extension method che "traduce" la specifica operazione asincrona di WinRT nel suo equivalente .NET. In altri termini, sotto il cofano vi è una sostanziale identità tra i tipi Task
e Task<TResult>
, da un lato, e le interfacce asincrone esposte da WinRT.
Non a caso, le API asincrone di WinRT espongono un metodo AsTask
(presente in numerosi overload) che permette di trasformare uno qualunque dei future type (IAsyncOperation
, IAsyncAction
, ecc.) nel corrispondente oggetto Task
di .NET, in modo da poterne sfruttare i relativi pattern (come Task.WhenAny
, Task.WhenAll
, Task.ContinueWith
, e così via).
Il seguente codice mostra un esempio di come convertire un future type di WinRT in un task tramite il metodo AsTask
:
Task<StorageFile> task = picker.PickSingleFileAsync().AsTask();
È anche possibile eseguire l'operazione inversa, ovvero passare da un Task
a uno dei quattro future type di WinRT, sfruttando, a seconda del tipo di interfaccia da restituire, gli extension method AsAsyncAction e AsAsyncOperation<TResult> (esposti dall'assembly System.Runtime.WindowsRuntime
). Come il nome suggerisce, il primo metodo converte un Task
in un IAsyncAction
, mentre il secondo accetta un Task<TResult>
e restituisce un oggetto IAsyncOperation<TResult>
. Il seguente snippet mostra un esempio del primo metodo:
private IAsyncAction DoSomeWorkAsync(Int32 seconds)
{
return new Task(() =>
{
// codice da eseguire
}).AsAsyncAction();
}
Annullare operazioni asincrone in WinRT
Le API asincrone di WinRT prevedono diverse strade per cancellare una o più operazioni asincrone, trasferendo la richiesta di annullamento lungo l'eventuale catena di chiamate, a seconda che si preferisca seguire il Task-Based Asyncrhonous Pattern (TAP), e dunque lavorare con oggetti Task
, ovvero sfruttare direttamente il meccanismo offerto dai future type di WinRT.
Il seguente snippet utilizza l'extension method AsTask
per trasformare l'oggetto di tipo IAsyncOperation<StorageFile>
restituito dal metodo PickSingleFileAsync
nel corrispondente Task<StorageFile>
, passando contestualmente la richiesta di annullamento sotto forma di un oggetto CancellationToken
:
private async void ChooseFile_Click(object sender, RoutedEventArgs e)
{
try
{
var picker = new Windows.Storage.Pickers.FileOpenPicker();
picker.FileTypeFilter.Add(".txt");
CancellationTokenSource cts = new CancellationTokenSource();
this.SetTimeoutOperation(10, cts); // imposta un timeout di 10 secondi
var file = await picker.PickSingleFileAsync().AsTask(cts.Token);
string content = await Windows.Storage.FileIO.ReadTextAsync(file);
this.FileContentTextBlock.Text = content;
}
catch (OperationCanceledException ex)
{
this.FileContentTextBlock.Text = "Operazione annullata";
}
catch (Exception ex)
{
// gestire l'eccezione
}
}
private async void SetTimeoutOperation(int seconds, CancellationTokenSource cts)
{
await Task.Delay(seconds * 1000);
cts.Cancel();
}
Il metodo SetTimeoutOperation
imposta un time-out che provvede ad annullare automaticamente l'operazione di selezione di un file qualora l'utente non compia la selezione stessa entro l'intervallo di tempo ricevuto come parametro (il time-out decorre dal momento in cui l'utente ha cliccato sul pulsante, o meglio, da quando viene invocato il metodo PickFileSingleAsync
e mostrata la relativa interfaccia utente). Una volta scaduto il time-out, il codice invoca il metodo Cancel
della classe CancellationTokenSource
per inoltrare la richiesta di annullamento della relativa operazione (ed eventualmente per propagare la richiesta di annullamento lungo la catena di chiamate asincrone):
Per testare questo codice, potete usare la seguente definizione XAML come riferimento per la vostra MainPage.xaml.cs.
<Page
x:Class="Demo.Html.it.AsyncSample.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.AsyncSample.CS"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{StaticResource 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>
Se adesso cliccate sul pulsante Scegli un file e attendete dieci secondi da quando appare la user interface del file picker, noterete che, al termine dell'intervallo, l'applicazione tornerà nuovamente alla pagina iniziale, mostrando il messaggio "Operazione annullata".
Il motivo è che, per annullare l'operazione asincrona, viene sollevata un'eccezione di tipo OperationCanceledException
, la quale viene poi propagata fino al blocco catch
nel metodo ChooseFile_Click
, dove emerge in corrispondenza dell'istruzione await che precede la chiamata al metodo PickSingleFileAsync
. In questo modo, le successive righe di codice all'interno dello stesso blocco try
(ossia l'invocazione del metodo ReadTextAsync
e l'assegnazione del risultato alla relativa textbox) non verranno eseguite.
Un'alternativa all'uso di AsTask
è rappresentata dal metodo Cancel
esposto direttamente dall'interfaccia IAsyncInfo
, da cui derivano tutti e quattro i future type di WinRT. In questo caso, a essere sfruttato è direttamente il meccanismo interno alle API asincrone di WinRT, senza che sia necessario trasformare preventivamente il future type in un Task
per poi passare un token contenente la richiesta di cancellazione.
Il prossimo snippet mostra una versione modificata del codice sopra riportato che sfrutta direttamente il meccanismo interno dei future type di WinRT:
IAsyncOperation<StorageFile> myOp = null;
private async void ChooseFile_Click(object sender, RoutedEventArgs e)
{
try
{
var picker = new Windows.Storage.Pickers.FileOpenPicker();
picker.FileTypeFilter.Add(".txt");
this.SetTimeoutOperation(10); // imposta un timeout di 10 secondi
myOp = picker.PickSingleFileAsync();
var file = await myOp;
string content = await Windows.Storage.FileIO.ReadTextAsync(file);
this.FileContentTextBlock.Text = content;
}
catch (OperationCanceledException ex)
{
this.FileContentTextBlock.Text = "Operazione annullata";
}
catch (Exception ex)
{
// gestire l'eccezione
}
}
private async void SetTimeoutOperation(int seconds)
{
await Task.Delay(seconds * 1000);
if (myOp != null)
myOp.Cancel();
}
Monitorare l'esecuzione di un'operazione asincrona
Molte delle API asincrone di WinRT, soprattutto quelle che prevedono operazioni particolarmente lunghe (come ad esempio scaricare file e dati dal web), offrono la possibilità di monitorare lo stato di avanzamento di un'operazione. Nel TAP, l'interfaccia che standardizza la comunicazione dello stato di un'operazione asincrona è IProgress<TProgress>
, la cui definizione è la seguente:
public interface IProgress<in T>
{
void Report(T Value);
}
Passando un oggetto che implementa l'interfaccia IProgress<TProgress>
a un'operazione asincrona, è dunque possibile essere notificati circa lo stato di avanzamento della stessa. Come abbiamo già accennato, in WinRT non tutte le API asincrone supportano questa possibilità, ma solo le interfacce IAsyncActionWithProgress<TProgress>
e IAsyncOperation<TResult, TProgress>
.
Come per il CancellationToken
, anche in questo caso le API di WinRT non accettano direttamente un oggetto che implementi l'interfaccia IProgress<TResult>
, ma occorre preventivamente trasformare il future type in un task tramite il metodo AsTask
, già discusso in precedenza. Ove riferito a future type che supportano la notifica dello stato di avanzamento, infatti, la classe WindowsRuntimeSystemExtensions
espone numerosi overload che accettano un oggetto di tipo IProgress<TResult>
(assieme o meno a un CancellationToken
).
Il modo più semplice di ottenere un oggetto che implementi l'interfaccia IProgress<T>
è quello di creare un'istanza della classe Progress<T>
, dove T
dipende dal tipo restituito da
Ad esempio, se abbiamo un'API che fornisce informazioni sullo stato di avanzamento tramite un integer
, quello che dobbiamo fare è passare come parametro al costruttore della classe Progress<int>
un delegate di tipo Action<int>
, come mostrato nel seguente snippet.
IProgress<Int32> progress = new Progress<Int32>((value) =>
{
// codice che mostra lo stato di avanzamento
// in questo caso value è un numero intero
});
Questo delegate verrà invocato ogni volta si rendano disponibili nuovi dati sullo stato dell'operazione (il tipo di dati usato nella interfaccia dipende ovviamente dal tipo di operazione asincrona). Sarà l'API stessa a invocare il delegate, passando come parametro il valore che indica lo stato di avanzamento di un'operazione.
Prendiamo ad esempio il metodo RetrieveFeedAsync
della classe SyndicationClient
. L'extension method AsTask
accetta come parametro (da solo o con un cancellation token) un oggetto di tipo IProgress<RetrievalProgress>
, dove RetrievalProgress
rappresenta il tipo di oggetto che verrà passato come parametro al delegate. Il seguente codice chiarisce questo punto:
private async void DownloadFromWeb_Click(object sender, RoutedEventArgs e)
{
var uri = new Uri("http://rss.feedsportal.com/c/32275/f/438637/index.rss");
CancellationTokenSource cts = new CancellationTokenSource();
this.SetTimeoutOperation(30, cts);
IProgress<RetrievalProgress> progress = new Progress<RetrievalProgress>((p) =>
{
this.ProgressStatusTextBlock.Text = String.Format("Avanzamento: {0} bytes di {1}", p.BytesRetrieved, p.TotalBytesToRetrieve);
});
var client = new SyndicationClient();
var feed = await client.RetrieveFeedAsync(uri).AsTask(cts.Token, progress);
this.ProgressStatusTextBlock.Text += "\n Operazione completata";
}
private async void SetTimeoutOperation(int seconds, CancellationTokenSource cts)
{
await Task.Delay(seconds * 1000);
cts.Cancel();
}
Nell'esempio, ogni volta che verrà reso disponibile un nuovo aggiornamento verrà invocato il delegate passato al metodo AsTask
come secondo parametro, e conseguentemente aggiornata la UI.
Per testare questo codice, potete usare la seguente definizione XAML come riferimento per la vostra MainPage.xam.cs
.
<Page
x:Class="Demo.Html.it.AsyncPatterns.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.AsyncPatterns.CS"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<Button Click="DownloadFromWeb_Click">Avvia download</Button>
<TextBlock x:Name="ProgressStatusTextBlock" Width="Auto" FontSize="18"></TextBlock>
</StackPanel>
</Grid>
</Page>
In alternativa all'uso del metodo AsTask
, è possibile sfruttare l'handler esposto dalla proprietà Progress
esposta dalle interfacce IAsyncActionWithProgress<TProgress>
e IAsyncOperationWithProgress<TResult, TProgress>
(di tipo, rispettivamente, AsyncActionProgressHandler<TProgress>
e AsyncOperationProgressHandler<TResult, TProgress>
) passando il delegate da invocare al momento in cui nuovi aggiornamenti si renderanno disponibili.
Il prossimo snippet mostra un esempio:
private async void DownloadFromWeb_Click(object sender, RoutedEventArgs e)
{
(...)
IProgress<RetrievalProgress> progress = new Progress<RetrievalProgress>((p) =>
{
this.ProgressStatusTextBlock.Text = String.Format("Avanzamento: {0} bytes di {1}", p.BytesRetrieved, p.TotalBytesToRetrieve);
});
var client = new SyndicationClient();
var myOp = client.RetrieveFeedAsync(uri);
myOp.Progress += OnProgress;
await myOp;
this.ProgressStatusTextBlock.Text += "\n Operazione completata";
}
Implementare propri metodi asincroni che supportano la cancellazione e la notifica degli aggiornamenti
Nell'articolo precedente abbiamo visto come implementare un proprio metodo asincrono utilizzando la Task Parallel Library di .NET. In alcuni casi, tuttavia, può essere utile lavorare direttamente con i future type di WinRT (ad esempio, se state sviluppando un componente WinRT, i cui metodi asincroni dovranno poter essere chiamati da uno qualunque dei linguaggi supportati da WinRT).
In questo caso, è possibile sfruttare il metodo statico Run
(nei suoi vari overload) della classe AsyncInfo
per costruire un tipo di task compatibile con WinRT. In particolare, ognuna delle quattro versioni di questo metodo restituiscono una delle quattro interfacce proprie dei future type, a seconda che il relativo task supporti la cancellazione e/o la notifica dello stato dell'operazione.
Il codice che segue mostra un esempio di metodo asincrono che supporta sia la cancellazione, che l'aggiornamento sullo stato dell'operazione.
private async void DoSomething_Click(object sender, RoutedEventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
IProgress<Int32> progress = new Progress<Int32>((value) =>
{
this.ProgressStatusTextBlock.Text = String.Format("Progresso : {0}%", value);
});
var myOp = await DoSomeWorkAsync().AsTask(cts.Token, progress);
}
private IAsyncOperationWithProgress<Int32, Int32> DoSomeWorkAsync()
{
return AsyncInfo.Run<Int32, Int32>((token, progress) =>
Task.Run<Int32>(async () =>
{
var sum = 0;
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
sum += i;
await Task.Delay(1000);
progress.Report(i);
}
return sum;
}, token));
}
Potete usare la seguente definizione XAML per testare questo codice:
<Page
x:Class="Demo.Html.it.AsyncPatterns.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.AsyncPatterns.CS"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<Button Click="DoSomething_Click">Avvia Operazione</Button>
<TextBlock x:Name="ProgressStatusTextBlock" Width="Auto" FontSize="18"></TextBlock>
</StackPanel>
</Grid>
</Page>
Il SinchronizationContext
Prima di concludere questo articolo, vale la pena accennare al concetto di contesto di sincronizzazione. Il comportamento di default dell'istruzione await
è quello di catturare il SynchronizationContext corrente e di usarlo per sincronizzare l'esecuzione del codice successivo alla parola chiave stessa. Questa è la ragione per cui non è necessario scrivere codice di sincronizzazione (come Dispatcher
) quando si manipolano oggetti di user interface tramite metodi asincroni che sfruttano le parole chiave async
e await
.
Questo comportamento è quello ottimale quando si ha del codice che deve interagire direttamente con la user interface. Tuttavia, nel caso in cui vogliate sviluppare una vostra libreria asincrona che non deve interagire con la UI, questo comportamento potrebbe creare qualche problema di efficienza.
Si prenda ad esempio questo codice:
private async Task DoSomeworkAsync()
{
var result = await OperationAsync();
this.UpdateUI(result);
}
La parola chiave await
attende l'esecuzione del task (magari in un differente contesto di sincronizzazione) e, una volta terminato, reimposta lo stesso contesto di esecuzione iniziale. Se così non fosse, il codice del metodo UpdateUI
- che, come il nome suggerisce, visualizza a video il risultato dell'operazione asincrona - rischierebbe di essere eseguito in un thread diverso da quello iniziale (ossia il thread della UI), a meno di non utilizzare un qualche altro meccanismo di sincronizzazione.
Questa operazione ha ovviamente un costo in termini di performance che, nel caso in cui non dobbiamo lavorare con la UI, può essere evitato. È infatti possibile evitare questo calo di performance eseguendo il metodo successivo alla chiamata asincrona in un thread differente rispetto a quello nel quale il metodo DoSomeworkAsync
è stato inizialmente chiamato. Invocando il metodo ConfigureAwait
è possibile modificare questo comportamento, chiedendo al sistema di eseguire il codice successivo nello stesso thread usato dal task, ignorando il SynchronizationContext
esistente al momento della chiamata all'istruzione await
. Il prossimo snippet mostra questo punto
private async Task DoSomeworkAsync()
{
var result = await OperationAsync().ConfigureAwait(false);
this.OtherActivityThatDoesNotInvolveUI(result);
}