Condividi tramite


Utilizzo del modello asincrono basato su attività

Quando si utilizza il modello asincrono basato su attività (TAP) per gestire operazioni asincrone, è possibile utilizzare i callback per aspettare senza bloccare. Per le attività, questa operazione viene ottenuta tramite metodi come Task.ContinueWith. Il supporto asincrono basato sul linguaggio nasconde i callback consentendo l'attesa di operazioni asincrone all'interno del normale flusso di controllo e il codice generato dal compilatore fornisce lo stesso supporto a livello di API.

Sospensione dell'esecuzione con Await

È possibile utilizzare la parola chiave await in C# e l'operatore Await in Visual Basic per attendere in modo asincrono gli oggetti Task e Task<TResult>. Quando si è in attesa di un oggetto Task, l'espressione await è di tipo void. Quando si è in attesa di un oggetto Task<TResult>, l'espressione await è di tipo TResult. Un'espressione await deve essere presente all'interno del corpo di un metodo asincrono. Queste funzionalità del linguaggio sono state introdotte in .NET Framework 4.5.

Sotto le quinte, la funzionalità await installa un callback nell'attività usando una continuazione. Questo callback riprende il metodo asincrono al punto di sospensione. Quando il metodo asincrono viene ripreso, se l'operazione attesa è stata completata correttamente ed era un Task<TResult>, viene restituito il suo TResult. Se il Task o il Task<TResult> che era atteso è terminato nello stato Canceled, viene lanciata un'eccezione OperationCanceledException. Se il Task o Task<TResult> che era atteso terminava nello stato Faulted, viene generata l'eccezione che ha causato l'errore. Un Task può generare un errore in seguito a più eccezioni, ma viene propagata solo una di queste eccezioni. Tuttavia, la Task.Exception proprietà restituisce un'eccezione AggregateException che contiene tutti gli errori.

Se un oggetto contesto di sincronizzazione (SynchronizationContext) è associato al thread che stava eseguendo il metodo asincrono al momento della sospensione (ad esempio, se la proprietà SynchronizationContext.Current non è null), il metodo asincrono riprende nello stesso contesto di sincronizzazione usando il metodo del contesto Post. In caso contrario, si basa sul task scheduler (TaskScheduler oggetto) che era attuale al momento della sospensione. Generalmente, si tratta del pianificatore di attività predefinito (TaskScheduler.Default), che ha come destinatario il pool di thread. Questo scheduler determina se l'operazione asincrona in attesa debba riprendere dal punto in cui è stata completata o se la ripresa debba essere pianificata. Il pianificatore predefinito consente in genere la continuazione dell'esecuzione nel thread in cui l'operazione attesa è stata completata.

Quando viene chiamato un metodo asincrono, il corpo della funzione viene eseguito in modo sincrono fino a raggiungere la prima espressione await su un'istanza attendibile che non è ancora stata completata, momento in cui l'invocazione torna al chiamante. Se il metodo asincrono non restituisce void, viene restituito un Task oggetto o Task<TResult> per rappresentare il calcolo in corso. In un metodo asincrono non void, se viene rilevata un'istruzione return o viene raggiunta la fine del corpo del metodo, l'attività viene completata nello RanToCompletion stato finale. Se un'eccezione non gestita fa sì che il controllo lasci il corpo del metodo asincrono, l'attività termina nello Faulted stato. Se quell'eccezione è un OperationCanceledException, l'attività termina nello stato Canceled invece. In questo modo, il risultato o l'eccezione viene infine pubblicato.

Esistono diverse varianti importanti di questo comportamento. Per motivi di prestazioni, se un'attività è già stata completata dal momento in cui l'attività è attesa, il controllo non viene restituito e la funzione continua a essere eseguita. Inoltre, tornare al contesto originale non è sempre il comportamento desiderato e può essere modificato; questo è descritto in modo più dettagliato nella sezione successiva.

Configurazione della sospensione e della ripresa con Yield e ConfigureAwait

Diversi metodi forniscono un maggiore controllo sull'esecuzione di un metodo asincrono. Ad esempio, è possibile usare il Task.Yield metodo per introdurre un punto di resa nel metodo asincrono:

public class Task : …
{
    public static YieldAwaitable Yield();
    …
}

Equivale a pubblicare o pianificare in modo asincrono nel contesto corrente.

Task.Run(async delegate
{
    for(int i=0; i<1000000; i++)
    {
        await Task.Yield(); // fork the continuation into a separate work item
        ...
    }
});

È anche possibile usare il Task.ConfigureAwait metodo per controllare meglio la sospensione e la ripresa in un metodo asincrono. Come accennato in precedenza, per impostazione predefinita, il contesto corrente viene acquisito al momento della sospensione di un metodo asincrono e tale contesto acquisito viene usato per richiamare la continuazione del metodo asincrono al momento della ripresa. In molti casi, si tratta del comportamento esatto desiderato. In altri casi, potrebbe non essere necessario preoccuparsi del contesto di prosecuzione e si possono ottenere prestazioni migliori evitando di ritornare al contesto originale. Per abilitare questa operazione, usare il Task.ConfigureAwait metodo per informare l'operazione await di non acquisire e non riprendere nel contesto, ma di continuare l'esecuzione ovunque l'operazione asincrona che era in attesa di completare si sia conclusa.

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Annullamento di un'operazione asincrona

A partire da .NET Framework 4, i metodi TAP che supportano l'annullamento forniscono almeno un overload che accetta un token di annullamento (CancellationToken oggetto ).

Un token di annullamento viene creato tramite una sorgente di token di annullamento (oggetto CancellationTokenSource). La proprietà dell'origine Token restituisce il token di annullamento che verrà segnalato quando viene chiamato il metodo Cancel dell'origine. Ad esempio, se si desidera scaricare una singola pagina Web e si vuole essere in grado di annullare l'operazione, creare un CancellationTokenSource oggetto, passarne il token al metodo TAP e quindi chiamare il metodo dell'origine Cancel quando si è pronti per annullare l'operazione:

var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

Per annullare più chiamate asincrone, è possibile passare lo stesso token a tutte le chiamate:

var cts = new CancellationTokenSource();
    IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
    // at some point later, potentially on another thread
    …
    cts.Cancel();

In alternativa, è possibile passare lo stesso token a un subset selettivo di operazioni:

var cts = new CancellationTokenSource();
    byte [] data = await DownloadDataAsync(url, cts.Token);
    await SaveToDiskAsync(outputPath, data, CancellationToken.None);
    … // at some point later, potentially on another thread
    cts.Cancel();

Importante

Le richieste di annullamento possono essere avviate da qualsiasi thread.

È possibile passare il CancellationToken.None valore a qualsiasi metodo che accetta un token di annullamento per indicare che l'annullamento non verrà mai richiesto. In questo modo la CancellationToken.CanBeCanceled proprietà restituisce falsee il metodo chiamato può ottimizzare di conseguenza. Ai fini del test, è possibile passare un token di annullamento già annullato, creato tramite il costruttore che accetta un valore booleano per indicare se il token deve iniziare in uno stato già annullato o in uno stato non annullabile.

Questo approccio all'annullamento presenta diversi vantaggi:

  • È possibile passare lo stesso token di annullamento a un numero qualsiasi di operazioni asincrone e sincrone.

  • La stessa richiesta di annullamento può essere distribuita a un numero qualsiasi di ascoltatori.

  • Lo sviluppatore dell'API asincrona ha il completo controllo sulla possibilità di richiedere la cancellazione e sui tempi della sua effettiva applicazione.

  • Il codice che utilizza l'API può determinare in modo selettivo le chiamate asincrone a cui verranno propagate le richieste di annullamento.

Monitoraggio dello stato di avanzamento

Alcuni metodi asincroni espongono lo stato di avanzamento tramite un'interfaccia di stato passata al metodo asincrono. Si consideri, ad esempio, una funzione che scarica in modo asincrono una stringa di testo e, lungo il percorso, genera aggiornamenti dello stato di avanzamento che includono la percentuale del download completato finora. Un metodo di questo tipo può essere utilizzato in un'applicazione Windows Presentation Foundation (WPF) come indicato di seguito:

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
            new Progress<int>(p => pbDownloadProgress.Value = p));
    }
    finally { btnDownload.IsEnabled = true; }
}

Uso dei combinatori predefiniti basati su attività

Lo System.Threading.Tasks namespace include diversi metodi per la composizione e l'utilizzo delle attività.

Metodo Task.Run

La classe Task include vari metodi Run che ti consentono di spostare facilmente il lavoro nel pool di thread come Task o Task<TResult>, ad esempio:

public async void button1_Click(object sender, EventArgs e)
{
    textBox1.Text = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer;
    });
}

Alcuni di questi Run metodi, ad esempio l'Task.Run(Func<Task>) overload, esistono come sintassi abbreviata per il metodo TaskFactory.StartNew. Questo sovraccarico consente di usare await all'interno del lavoro decentrato, ad esempio:

public async void button1_Click(object sender, EventArgs e)
{
    pictureBox1.Image = await Task.Run(async() =>
    {
        using(Bitmap bmp1 = await DownloadFirstImageAsync())
        using(Bitmap bmp2 = await DownloadSecondImageAsync())
        return Mashup(bmp1, bmp2);
    });
}

Tali overload sono logicamente equivalenti all'uso del metodo TaskFactory.StartNew insieme al metodo di estensione Unwrap nella "Task Parallel Library".

Task.FromResult

Usare il FromResult metodo negli scenari in cui i dati potrebbero essere già disponibili e devono essere restituiti solo da un metodo di restituzione di attività sollevato in un Task<TResult>oggetto :

public Task<int> GetValueAsync(string key)
{
    int cachedValue;
    return TryGetCachedValue(out cachedValue) ?
        Task.FromResult(cachedValue) :
        GetValueAsyncInternal();
}

private async Task<int> GetValueAsyncInternal(string key)
{
    …
}

Task.WhenAll

Usare il WhenAll metodo per attendere in modo asincrono più operazioni asincrone rappresentate come attività. Il metodo include più overload che supportano un insieme di attività non generiche o un insieme non uniforme di attività generiche (ad esempio, l'attesa asincrona di più operazioni che restituiscono void o l'attesa asincrona di più metodi che restituiscono valori, in cui ogni valore può avere un tipo diverso) e per supportare un insieme uniforme di attività generiche (come l'attesa asincrona di più metodi che restituiscono valori TResult).

Si supponga di voler inviare messaggi di posta elettronica a diversi clienti. È possibile sovrapporre l'invio dei messaggi in modo da non attendere il completamento di un messaggio prima di inviare il successivo. È anche possibile scoprire quando le operazioni di invio sono state completate e se si sono verificati errori:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

Questo codice non gestisce in modo esplicito le eccezioni che possono verificarsi, ma lascia che le eccezioni si propaghino fuori dall'attività risultante da await di WhenAll. Per gestire le eccezioni, è possibile usare codice come il seguente:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    ...
}

In questo caso, se un'operazione asincrona ha esito negativo, tutte le eccezioni verranno consolidate in un'eccezione AggregateException , archiviata nell'oggetto Task restituito dal WhenAll metodo . Tuttavia, solo una di queste eccezioni viene propagata dalla await parola chiave . Se si desidera esaminare tutte le eccezioni, è possibile riscrivere il codice precedente nel modo seguente:

Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Si consideri un esempio di download asincrono di più file dal Web. In questo caso, tutte le operazioni asincrone hanno tipi di risultati omogenei ed è facile accedere ai risultati:

string [] pages = await Task.WhenAll(
    from url in urls select DownloadStringTaskAsync(url));

È possibile utilizzare le stesse tecniche di gestione delle eccezioni di cui abbiamo parlato nello scenario precedente in cui il metodo restituisce void.

Task<string> [] asyncOps =
    (from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
    string [] pages = await Task.WhenAll(asyncOps);
    ...
}
catch(Exception exc)
{
    foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Task.WhenAny

È possibile usare il WhenAny metodo per attendere in modo asincrono solo una delle più operazioni asincrone rappresentate come attività da completare. Questo metodo serve quattro casi d'uso principali:

  • Ridondanza: l'esecuzione di un'operazione più volte e la selezione di quella che viene completata per prima (ad esempio, contattare più servizi Web di offerta azionaria che produrranno un singolo risultato e selezionare quella che completa il più veloce).

  • Interleaving: avvio di più operazioni e attesa del completamento di tutte, elaborandole man mano che finiscono.

  • Limitazione: consente l'avvio di operazioni aggiuntive man mano che altre operazioni vengono completate. Si tratta di un'estensione dello scenario di interleaving.

  • Salvataggio anticipato: ad esempio, un'operazione rappresentata dall'attività t1 può essere raggruppata in un'attività WhenAny con un'altra attività t2 ed è possibile attendere l'attività WhenAny . L'attività t2 potrebbe rappresentare un timeout, un annullamento o un altro segnale che fa sì che l'attività WhenAny si completi prima che t1 sia completata.

Ridondanza

Si consideri un caso in cui si vuole prendere una decisione su se acquistare un titolo. Esistono diversi servizi Web di raccomandazione azionari attendibili, ma a seconda del carico giornaliero, ogni servizio può risultare lento in momenti diversi. È possibile usare il WhenAny metodo per ricevere una notifica al termine di un'operazione:

var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol),
    GetBuyRecommendation2Async(symbol),
    GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);

A differenza di WhenAll, che restituisce i risultati non compressi di tutte le attività completate correttamente, WhenAny restituisce l'attività completata. Se un'attività ha esito negativo, è importante sapere che non è riuscita e, se un'attività ha esito positivo, è importante sapere a quale attività è associato il valore restituito. Pertanto, è necessario accedere al risultato dell'attività restituita o attendere ulteriormente, come illustrato in questo esempio.

Come per WhenAll, è necessario essere in grado di supportare le eccezioni. Poiché si riceve nuovamente l'attività completata, è possibile attendere che l'attività restituita abbia la propagazione degli errori e gestirli in modo appropriato; ad esempio:

Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    try
    {
        if (await recommendation) BuyStock(symbol);
        break;
    }
    catch(WebException exc)
    {
        recommendations.Remove(recommendation);
    }
}

Inoltre, anche se una prima attività viene completata correttamente, le attività successive potrebbero non riuscire. A questo punto, sono disponibili diverse opzioni per gestire le eccezioni: è possibile attendere il completamento di tutte le attività avviate, nel qual caso è possibile usare il WhenAll metodo oppure decidere che tutte le eccezioni sono importanti e devono essere registrate. A tale scopo, è possibile usare le continuazioni per ricevere una notifica quando le attività sono state completate in modo asincrono:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => { if (t.IsFaulted) Log(t.Exception); });
}

oppure:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

o addirittura:

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach(var task in tasks)
    {
        try { await task; }
        catch(Exception exc) { Log(exc); }
    }
}
…
LogCompletionIfFailed(recommendations);

Infine, è possibile annullare tutte le operazioni rimanenti:

var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol, cts.Token),
    GetBuyRecommendation2Async(symbol, cts.Token),
    GetBuyRecommendation3Async(symbol, cts.Token)
};

Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);

Interlacciamento

Si consideri un caso in cui si scaricano immagini dal Web e si elabora ogni immagine (ad esempio, aggiungendo l'immagine a un controllo dell'interfaccia utente). Le immagini vengono elaborate in sequenza nel thread dell'interfaccia utente, ma si desidera scaricarle il più possibile in modo simultaneo. Inoltre, non vuoi ritardare l'aggiunta delle immagini all'interfaccia utente finché non sono state tutte scaricate. Aggiungili man mano che vengono completati.

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

È anche possibile applicare l'interlacciamento a uno scenario che comporta un'elaborazione computazionale intensiva delle immagini scaricate, ad esempio:

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)
         .ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Regolazione

Si consideri l'esempio di interleaving, ad eccezione del fatto che l'utente sta scaricando così tante immagini che i download devono essere limitati; Ad esempio, si vuole che venga eseguito simultaneamente solo un numero specifico di download. A tale scopo, è possibile avviare un subset delle operazioni asincrone. Al termine delle operazioni, è possibile avviare operazioni aggiuntive per sostituirle.

const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
    imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
    nextIndex++;
}

while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch(Exception exc) { Log(exc); }

    if (nextIndex < urls.Length)
    {
        imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
        nextIndex++;
    }
}

Salvataggio anticipato

Si consideri che si sta aspettando in modo asincrono il completamento di un'operazione durante la risposta simultanea alla richiesta di annullamento di un utente( ad esempio, l'utente ha fatto clic su un pulsante annulla). Il codice seguente illustra questo scenario:

private CancellationTokenSource m_cts;

public void btnCancel_Click(object sender, EventArgs e)
{
    if (m_cts != null) m_cts.Cancel();
}

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();
    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        if (imageDownload.IsCompleted)
        {
            Bitmap image = await imageDownload;
            panel.AddImage(image);
        }
        else imageDownload.ContinueWith(t => Log(t));
    }
    finally { btnRun.Enabled = true; }
}

private static async Task UntilCompletionOrCancellation(
    Task asyncOp, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<bool>();
    using(ct.Register(() => tcs.TrySetResult(true)))
        await Task.WhenAny(asyncOp, tcs.Task);
    return asyncOp;
}

Questa implementazione riabilita l'interfaccia utente non appena si decide di abbandonare, ma non annulla le operazioni asincrone sottostanti. Un'altra alternativa consiste nell'annullare le operazioni in sospeso quando si decide di salvare, ma non ristabilire l'interfaccia utente fino al completamento delle operazioni, potenzialmente a causa del termine anticipato a causa della richiesta di annullamento:

private CancellationTokenSource m_cts;

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();

    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        Bitmap image = await imageDownload;
        panel.AddImage(image);
    }
    catch(OperationCanceledException) {}
    finally { btnRun.Enabled = true; }
}

Un altro esempio di bailout anticipato prevede l'uso del WhenAny metodo insieme al Delay metodo , come illustrato nella sezione successiva.

Task.Delay

È possibile usare il Task.Delay metodo per introdurre pause nell'esecuzione di un metodo asincrono. Ciò è utile per molti tipi di funzionalità, tra cui la creazione di cicli di polling e il ritardo della gestione dell'input dell'utente per un periodo di tempo predeterminato. Il Task.Delay metodo può essere utile anche in combinazione con Task.WhenAny per l'implementazione dei timeout in await.

Se un'attività che fa parte di un'operazione asincrona più grande (ad esempio, un servizio Web di ASP.NET) richiede troppo tempo, l'operazione complessiva potrebbe risentire, soprattutto se non riesce a essere completata. Per questo motivo, è importante saper impostare un limite di tempo quando si attende un'operazione asincrona. I metodi sincroni Task.Wait, Task.WaitAll, e Task.WaitAny accettano valori di timeout, ma i corrispondenti metodi TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny e quelli precedentemente indicati Task.WhenAll/Task.WhenAny non lo accettano. È invece possibile usare Task.Delay e Task.WhenAny in combinazione per implementare un timeout.

Ad esempio, nell'applicazione dell'interfaccia utente si supponga di voler scaricare un'immagine e disabilitare l'interfaccia utente durante il download dell'immagine. Tuttavia, se il download richiede troppo tempo, si vuole riabilitare l'interfaccia utente ed eliminare il download:

public async void btnDownload_Click(object sender, EventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap> download = GetBitmapAsync(url);
        if (download == await Task.WhenAny(download, Task.Delay(3000)))
        {
            Bitmap bmp = await download;
            pictureBox.Image = bmp;
            status.Text = "Downloaded";
        }
        else
        {
            pictureBox.Image = null;
            status.Text = "Timed out";
            var ignored = download.ContinueWith(
                t => Trace("Task finally completed"));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Lo stesso vale per più download, perché WhenAll restituisce un'attività:

public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap[]> downloads =
            Task.WhenAll(from url in urls select GetBitmapAsync(url));
        if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
        {
            foreach(var bmp in downloads.Result) panel.AddImage(bmp);
            status.Text = "Downloaded";
        }
        else
        {
            status.Text = "Timed out";
            downloads.ContinueWith(t => Log(t));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Creazione di combinatori basati su attività

Poiché un'attività è in grado di rappresentare completamente un'operazione asincrona e fornire funzionalità sincrone e asincrone per l'unione con l'operazione, il recupero dei risultati e così via, è possibile creare librerie utili di combinatori che compongono attività per creare modelli più grandi. Come illustrato nella sezione precedente, .NET include diversi combinatori predefiniti, ma è anche possibile crearne uno personalizzato. Le sezioni seguenti forniscono diversi esempi di metodi e tipi di combinatore potenziali.

RetryOnFault

In molte situazioni, potrebbe essere necessario ritentare un'operazione se un tentativo precedente non riesce. Per il codice sincrono, è possibile compilare un metodo helper, RetryOnFault ad esempio nell'esempio seguente per eseguire questa operazione:

public static T RetryOnFault<T>(
    Func<T> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

È possibile creare un metodo helper quasi identico per le operazioni asincrone implementate con TAP e quindi restituire attività:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

È quindi possibile usare questo combinatore per codificare i tentativi nella logica dell'applicazione, per esempio:

// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3);

È possibile estendere ulteriormente la RetryOnFault funzione. Ad esempio, la funzione potrebbe accettare un altro Func<Task> che verrà richiamato tra i tentativi per determinare quando ritentare l'operazione, ad esempio:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T);
}

È quindi possibile usare la funzione come indicato di seguito per attendere un secondo prima di ripetere l'operazione:

// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOne

In alcuni casi, è possibile sfruttare la ridondanza per migliorare la latenza e le probabilità di successo di un'operazione. Prendere in considerazione più servizi Web che forniscono quotazioni azionarie, ma in vari momenti del giorno, ogni servizio può fornire diversi livelli di qualità e tempi di risposta. Per gestire queste fluttuazioni, è possibile inviare richieste a tutti i servizi Web e non appena si riceve una risposta da una, annullare le richieste rimanenti. È possibile implementare una funzione helper per semplificare l'implementazione di questo modello comune di avvio di più operazioni, in attesa di qualsiasi operazione e quindi per annullare il resto. La NeedOnlyOne funzione nell'esempio seguente illustra questo scenario:

public static async Task<T> NeedOnlyOne(
    params Func<CancellationToken,Task<T>> [] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach(var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return completed;
}

È quindi possibile usare questa funzione come segue:

double currentPrice = await NeedOnlyOne(
    ct => GetCurrentPriceFromServer1Async("msft", ct),
    ct => GetCurrentPriceFromServer2Async("msft", ct),
    ct => GetCurrentPriceFromServer3Async("msft", ct));

Operazioni interleaved

Può verificarsi un potenziale problema di prestazioni utilizzando il metodo WhenAny per supportare uno scenario di interleaving quando si lavorano con insiemi di attività di grandi dimensioni. Ogni chiamata a WhenAny comporta la registrazione del proseguimento per ogni attività. Per un numero N di attività, si ottengono continuazioni O(N2) create nell'arco della durata dell'operazione di interleaving. Se si usa un set di attività di grandi dimensioni, è possibile usare un combinatore (Interleaved nell'esempio seguente) per risolvere il problema di prestazioni:

static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}

È quindi possibile usare il combinatore per elaborare i risultati delle attività al termine; Per esempio:

IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
    int result = await task;
    …
}

WhenAllOrFirstException

In alcuni scenari di dispersione/raccolta, potrebbe essere necessario attendere tutte le attività in un set, a meno che una di esse generi un errore, nel qual caso si desidera interrompere l'attesa non appena si verifica l'eccezione. A tale scopo, è possibile usare un metodo combinatore, WhenAllOrFirstException ad esempio nell'esempio seguente:

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}

Compilazione di strutture di dati basate su attività

Oltre alla possibilità di creare combinatori personalizzati basati su attività, avere una struttura di dati in Task e Task<TResult> che rappresenta sia i risultati di un'operazione asincrona che la sincronizzazione necessaria per l'aggiunta a esso rende un tipo potente su cui compilare strutture di dati personalizzate da usare in scenari asincroni.

AsyncCache

Un aspetto importante di un'attività è che può essere distribuita a più consumatori, tutti i quali possono attenderla, registrare le continuazioni ad essa, ottenere il risultato o le eccezioni (nel caso di Task<TResult>) e così via. Ciò rende Task e Task<TResult> perfettamente adatto per essere usato in un'infrastruttura di memorizzazione nella cache asincrona. Ecco un esempio di una cache asincrona piccola ma potente basata su Task<TResult>:

public class AsyncCache<TKey, TValue>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException("valueFactory");
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException("key");
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}

La classe AsyncCache<TKey,TValue> accetta come delegato al suo costruttore una funzione che prende un TKey e restituisce un Task<TResult>. Tutti i valori a cui si accede in precedenza dalla cache vengono archiviati nel dizionario interno e AsyncCache garantisce che venga generata una sola attività per chiave, anche se la cache è accessibile contemporaneamente.

Ad esempio, è possibile compilare una cache per le pagine Web scaricate:

private AsyncCache<string,string> m_webPages =
    new AsyncCache<string,string>(DownloadStringTaskAsync);

È quindi possibile usare questa cache nei metodi asincroni ogni volta che è necessario il contenuto di una pagina Web. La AsyncCache classe garantisce il download del minor numero possibile di pagine e memorizza nella cache i risultati.

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtContents.Text = await m_webPages["https://www.microsoft.com"];
    }
    finally { btnDownload.IsEnabled = true; }
}

AsyncProducerConsumerCollection

È anche possibile usare le attività per creare strutture di dati per coordinare le attività asincrone. Si consideri uno dei modelli di progettazione parallela classici: producer/consumer. In questo modello, i produttori generano dati che vengono consumati dai consumatori, e i produttori e i consumatori possono essere eseguiti in parallelo. Ad esempio, il consumer elabora l'elemento 1, generato in precedenza da un produttore che ora produce l'elemento 2. Per il modello producer/consumer, è necessario invariabilmente una struttura di dati per archiviare il lavoro creato dai produttori in modo che i consumer possano ricevere una notifica dei nuovi dati e trovarli quando disponibili.

Ecco una semplice struttura dati, basata su attività, che consente di utilizzare metodi asincroni come produttori e consumatori:

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T> tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}

Con tale struttura di dati, è possibile scrivere codice come il seguente:

private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.Take();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Add(data);
}

Lo System.Threading.Tasks.Dataflow spazio dei nomi include il BufferBlock<T> tipo, che è possibile usare in modo simile, ma senza dover creare un tipo di raccolta personalizzato.

private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.ReceiveAsync();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Post(data);
}

Annotazioni

Lo System.Threading.Tasks.Dataflow spazio dei nomi è disponibile come pacchetto NuGet. Per installare l'assembly che contiene lo System.Threading.Tasks.Dataflow spazio dei nomi, aprire il progetto in Visual Studio, scegliere Gestisci pacchetti NuGet dal menu Progetto e cercare il pacchetto System.Threading.Tasks.Dataflow online.

Vedere anche