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à, questo modello usa 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 attende un Task, l'espressione await è di tipo void. Quando si attende un 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 si chiama un metodo asincrono, esegue in modo sincrono il corpo della funzione fino alla prima espressione await in un'istanza awaitable non ancora completata, a quel punto la chiamata torna al chiamante. Se il metodo asincrono non restituisce void, restituisce 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 entro il 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 comportamento è 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();
    …
}

Questo metodo equivale a inviare o pianificare in modo asincrono per ritornare al contesto corrente.

public static async Task YieldLoopExample()
{
    await Task.Run(async delegate
    {
        for (int i = 0; i < 1000000; i++)
        {
            await Task.Yield(); // fork the continuation into a separate work item
        }
    });
}
Public Async Function YieldLoopExample() As Task
    Await Task.Run(Async Function()
                       For i As Integer = 0 To 999999
                           Await Task.Yield() ' fork the continuation into a separate work item
                       Next
                   End Function)
End Function

È 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 interessare il contesto di continuazione ed è possibile ottenere prestazioni migliori evitando tali post nel contesto originale. Per abilitare questo comportamento, usare il Task.ConfigureAwait metodo per informare l'operazione await di non acquisire e riprendere il contesto, ma di continuare l'esecuzione ovunque sia stata completata l'operazione asincrona in attesa:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Awaitables, ConfigureAwait e SynchronizationContext

await funziona con qualsiasi tipo che soddisfi lo schema di espressione awaitable, non solo Task. Un tipo è awaitable se fornisce un metodo compatibile GetAwaiter che restituisce un tipo con membri IsCompleted, OnCompleted e GetResult. Nella maggior parte delle API pubbliche restituire Task, Task<TResult>, ValueTasko ValueTask<TResult>. Usare awaitable personalizzati solo per scenari specializzati.

Usare ConfigureAwait quando la continuazione non richiede il contesto del chiamante. Nel codice dell'app che aggiorna un'interfaccia utente, l'acquisizione del contesto è spesso necessaria. Nel codice della libreria riutilizzabile, ConfigureAwait(false) è generalmente preferibile perché evita cambiamenti di contesto non necessari e riduce il rischio di deadlock per i chiamanti che causano blocchi.

ConfigureAwait(false) modifica la pianificazione della continuazione, non ExecutionContext il flusso. Per una spiegazione più approfondita del comportamento del contesto, vedere ExecutionContext e SynchronizationContext.

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 ).

Si crea un token di annullamento tramite una fonte di token di annullamento (oggetto CancellationTokenSource). La proprietà Token dell'origine restituisce il token di annullamento che segnala quando viene chiamato il metodo Cancel dell'origine.

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

Ad esempio, se si vuole 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();
    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

Qualsiasi thread può avviare richieste di annullamento.

È possibile passare il valore a qualsiasi metodo che accetta un token di annullamento per indicare che l'annullamento CancellationToken.None non viene mai richiesto. Questo valore fa sì che la CancellationToken.CanBeCanceled proprietà restituisca 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 inviata a un numero qualsiasi di listener.

  • Lo sviluppatore dell'API asincrona ha il controllo completo sul fatto che l'annullamento possa essere richiesto e quando diventa effettivo.

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

Monitoraggio dello stato di avanzamento

Alcuni metodi asincroni espongono lo stato di avanzamento tramite un'interfaccia di progresso 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. È possibile utilizzare tale metodo in un'applicazione Windows Presentation Foundation (macchine virtuali Windows) 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à.

Annotazioni

Diversi esempi di codice in questa sezione usano , che richiede il pacchetto />

Metodo Task.Run

La Task classe include diversi Run metodi che consentono di eseguire facilmente l'offload dei lavori come Task o Task<TResult> al pool di thread. Per esempio:

public static async Task TaskRunBasicExample()
{
    int answer = 42;
    string result = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer.ToString();
    });
    Console.WriteLine(result);
}
Public Async Function TaskRunBasicExample() As Task
    Dim answer As Integer = 42
    Dim result As String = Await Task.Run(Function()
                                              ' … do compute-bound work here
                                              Return answer.ToString()
                                          End Function)
    Console.WriteLine(result)
End Function

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

public static async Task TaskRunAsyncExample()
{
    Bitmap image = await Task.Run(async () =>
    {
        using Bitmap bmp1 = await Stubs.DownloadFirstImageAsync();
        using Bitmap bmp2 = await Stubs.DownloadSecondImageAsync();
        return Stubs.Mashup(bmp1, bmp2);
    });
}
Public Async Function TaskRunAsyncExample() As Task
    Dim image As Bitmap = Await Task.Run(Async Function()
                                             Using bmp1 As Bitmap = Await Stubs.DownloadFirstImageAsync()
                                                 Using bmp2 As Bitmap = Await Stubs.DownloadSecondImageAsync()
                                                     Return Stubs.Mashup(bmp1, bmp2)
                                                 End Using
                                             End Using
                                         End Function)
End Function

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 ed è sufficiente restituirli tramite un metodo con ritorno di attività elevata in un Task<TResult>.

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

static async Task<int> GetValueAsyncInternal(string key)
{
    await Task.Delay(1);
    return 0;
}
Public Function GetValueAsync(key As String) As Task(Of Integer)
    Dim cachedValue As Integer
    If Stubs.TryGetCachedValue(cachedValue) Then
        Return Task.FromResult(cachedValue)
    Else
        Return GetValueAsyncInternal(key)
    End If
End Function

Private Async Function GetValueAsyncInternal(key As String) As Task(Of Integer)
    Await Task.Delay(1)
    Return 0
End Function

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 set di attività non generiche o un set 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 set uniforme di attività generiche (ad esempio, in attesa asincrona di più metodi che restituiscono 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 vengono completate e se si verificano 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 potrebbero verificarsi, ma permette alle eccezioni di propagare dall'attività await risultante da WhenAll. Per gestire le eccezioni, usare codice come il seguente:

public static async Task WhenAllWithCatch()
{
    IEnumerable<Task> asyncOps = from addr in Stubs.addrs select Stubs.SendMailAsync(addr);
    try
    {
        await Task.WhenAll(asyncOps);
    }
    catch (Exception exc)
    {
        Console.WriteLine(exc);
    }
}
Public Async Function WhenAllWithCatch() As Task
    Dim asyncOps As IEnumerable(Of Task) = From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)
    Try
        Await Task.WhenAll(asyncOps)
    Catch exc As Exception
        Console.WriteLine(exc)
    End Try
End Function

In questo caso, se un'operazione asincrona ha esito negativo, tutte le eccezioni vengono 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:

public static async Task WhenAllExamineExceptions()
{
    Task[] asyncOps = (from addr in Stubs.addrs select Stubs.SendMailAsync(addr)).ToArray();
    try
    {
        await Task.WhenAll(asyncOps);
    }
    catch (Exception exc)
    {
        foreach (Task faulted in asyncOps.Where(t => t.IsFaulted))
        {
            Console.WriteLine($"Faulted: {faulted.Exception}");
        }
    }
}
Public Async Function WhenAllExamineExceptions() As Task
    Dim asyncOps As Task() = (From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)).ToArray()
    Try
        Await Task.WhenAll(asyncOps)
    Catch exc As Exception
        For Each faulted As Task In asyncOps.Where(Function(t) t.IsFaulted)
            Console.WriteLine($"Faulted: {faulted.Exception}")
        Next
    End Try
End Function

Si consideri un esempio di download di più file dal Web in modo asincrono. 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 usare le stesse tecniche di gestione delle eccezioni descritte nello scenario precedente void-returning:

public static async Task WhenAllDownloadPagesExceptions()
{
    Task<string>[] asyncOps =
        (from url in Stubs.urls select Stubs.DownloadStringTaskAsync(url)).ToArray();
    try
    {
        string[] pages = await Task.WhenAll(asyncOps);
        Console.WriteLine(pages.Length);
    }
    catch (Exception exc)
    {
        foreach (Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
        {
            Console.WriteLine($"Faulted: {faulted.Exception}");
        }
    }
}
Public Async Function WhenAllDownloadPagesExceptions() As Task
    Dim asyncOps As Task(Of String)() =
        (From url In Stubs.urls Select Stubs.DownloadStringTaskAsync(url)).ToArray()
    Try
        Dim pages As String() = Await Task.WhenAll(asyncOps)
        Console.WriteLine(pages.Length)
    Catch exc As Exception
        For Each faulted As Task(Of String) In asyncOps.Where(Function(t) t.IsFaulted)
            Console.WriteLine($"Faulted: {faulted.Exception}")
        Next
    End Try
End Function

Task.WhenAny

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: esecuzione di un'operazione più volte e selezione di quella che viene completata per prima (ad esempio, contattare più servizi Web di offerta azionaria che restituiscono 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. Questo scenario è 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 che si considerano attendibili, ma a seconda del carico giornaliero, ogni servizio può risultare lento in momenti diversi. Usare il WhenAny metodo per ricevere una notifica al termine di un'operazione:

public static async Task WhenAnyRedundancy(string symbol)
{
    var recommendations = new List<Task<bool>>()
    {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    };
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyRedundancy(symbol As String) As Task
    Dim recommendations As New List(Of Task(Of Boolean)) From {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    }
    Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
    If Await recommendation Then Stubs.BuyStock(symbol)
End Function

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:

public static async Task WhenAnyRetryOnException(string symbol)
{
    Task<bool>[] allRecommendations = new Task<bool>[]
    {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    };
    var remaining = allRecommendations.ToList();
    while (remaining.Count > 0)
    {
        Task<bool> recommendation = await Task.WhenAny(remaining);
        try
        {
            if (await recommendation) Stubs.BuyStock(symbol);
            break;
        }
        catch (WebException)
        {
            remaining.Remove(recommendation);
        }
    }
}
Public Async Function WhenAnyRetryOnException(symbol As String) As Task
    Dim allRecommendations As Task(Of Boolean)() = {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    }
    Dim remaining As List(Of Task(Of Boolean)) = allRecommendations.ToList()
    While remaining.Count > 0
        Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(remaining)
        Try
            If Await recommendation Then Stubs.BuyStock(symbol)
            Exit While
        Catch ex As WebException
            remaining.Remove(recommendation)
        End Try
    End While
End Function

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. Per questo scenario, è possibile usare le continuazioni per ricevere una notifica quando le attività vengono 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) { Stubs.Log(exc); }
    }
}
Private Async Sub LogCompletionIfFailed(tasks As IEnumerable(Of Task))
    For Each task In tasks
        Try
            Await task
        Catch exc As Exception
            Stubs.Log(exc)
        End Try
    Next
End Sub

Infine, è possibile annullare tutte le operazioni rimanenti:

public static async Task WhenAnyCancelRemainder(string symbol)
{
    var cts = new CancellationTokenSource();
    var recommendations = new List<Task<bool>>()
    {
        Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
    };

    Task<bool> recommendation = await Task.WhenAny(recommendations);
    cts.Cancel();
    if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyCancelRemainder(symbol As String) As Task
    Dim cts As New CancellationTokenSource()
    Dim recommendations As New List(Of Task(Of Boolean)) From {
        Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
    }

    Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
    cts.Cancel()
    If Await recommendation Then Stubs.BuyStock(symbol)
End Function

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.

public static async Task WhenAnyInterleaving(string[] imageUrls)
{
    List<Task<Bitmap>> imageTasks =
        (from imageUrl in imageUrls select Stubs.GetBitmapAsync(imageUrl)).ToList();
    while (imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch { }
    }
}
Public Async Function WhenAnyInterleaving(imageUrls As String()) As Task
    Dim imageTasks As List(Of Task(Of Bitmap)) =
        (From imageUrl In imageUrls Select Stubs.GetBitmapAsync(imageUrl)).ToList()
    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch
        End Try
    End While
End Function

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

public static async Task WhenAnyInterleavingWithProcessing(string[] imageUrls)
{
    List<Task<Bitmap>> imageTasks =
        (from imageUrl in imageUrls
         select Stubs.GetBitmapAsync(imageUrl)
             .ContinueWith(t => Stubs.ConvertImage(t.Result))).ToList();
    while (imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch { }
    }
}
Public Async Function WhenAnyInterleavingWithProcessing(imageUrls As String()) As Task
    Dim imageTasks As List(Of Task(Of Bitmap)) =
        (From imageUrl In imageUrls
         Select Stubs.GetBitmapAsync(imageUrl).ContinueWith(Function(t) Stubs.ConvertImage(t.Result))).ToList()
    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch
        End Try
    End While
End Function

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. Per raggiungere questo obiettivo, avviare un sottoinsieme delle operazioni asincrone. Al termine delle operazioni, è possibile avviare operazioni aggiuntive per sostituirle.

public static async Task WhenAnyThrottling(Uri[] uriList)
{
    const int CONCURRENCY_LEVEL = 15;
    int nextIndex = 0;
    var imageTasks = new List<Task<Bitmap>>();
    while (nextIndex < CONCURRENCY_LEVEL && nextIndex < uriList.Length)
    {
        imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
        nextIndex++;
    }

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

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch (Exception exc) { Stubs.Log(exc); }

        if (nextIndex < uriList.Length)
        {
            imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
            nextIndex++;
        }
    }
}
Public Async Function WhenAnyThrottling(uriList As Uri()) As Task
    Const CONCURRENCY_LEVEL As Integer = 15
    Dim nextIndex As Integer = 0
    Dim imageTasks As New List(Of Task(Of Bitmap))
    While nextIndex < CONCURRENCY_LEVEL AndAlso nextIndex < uriList.Length
        imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
        nextIndex += 1
    End While

    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch exc As Exception
            Stubs.Log(exc)
        End Try

        If nextIndex < uriList.Length Then
            imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
            nextIndex += 1
        End If
    End While
End Function

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:

class EarlyBailoutUI
{
    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();
        try
        {
            Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url");
            await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
            if (imageDownload.IsCompleted)
            {
                Bitmap image = await imageDownload;
                Stubs.Log(image);
            }
            else imageDownload.ContinueWith(t => Stubs.Log(t));
        }
        finally { }
    }
}
Class EarlyBailoutUI
    Private m_cts As CancellationTokenSource

    Public Sub btnCancel_Click(sender As Object, e As EventArgs)
        If m_cts IsNot Nothing Then m_cts.Cancel()
    End Sub

    Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
        m_cts = New CancellationTokenSource()
        Try
            Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url")
            Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
            If imageDownload.IsCompleted Then
                Dim image As Bitmap = Await imageDownload
                Stubs.Log(image)
            Else
                imageDownload.ContinueWith(Sub(t) Stubs.Log(t))
            End If
        Finally
        End Try
    End Sub
End Class

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:

class EarlyBailoutWithTokenUI
{
    private CancellationTokenSource? m_cts;

    public async void btnRun_Click(object sender, EventArgs e)
    {
        m_cts = new CancellationTokenSource();
        try
        {
            Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url", m_cts.Token);
            await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
            Bitmap image = await imageDownload;
            Stubs.Log(image);
        }
        catch (OperationCanceledException) { }
        finally { }
    }
}
Class EarlyBailoutWithTokenUI
    Private m_cts As CancellationTokenSource

    Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
        m_cts = New CancellationTokenSource()
        Try
            Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url", m_cts.Token)
            Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
            Dim image As Bitmap = Await imageDownload
            Stubs.Log(image)
        Catch ex As OperationCanceledException
        Finally
        End Try
    End Sub
End Class

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

Task.Delay

Usare il Task.Delay metodo per aggiungere pause nell'esecuzione di un metodo asincrono. Questa pausa è 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. È anche possibile usare il Task.Delay metodo con Task.WhenAny per implementare i 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 metodi corrispondenti TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny e quelli menzionati in precedenza Task.WhenAll/Task.WhenAny non li accettano. Invece, utilizzare Task.Delay e Task.WhenAny insieme 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 static async Task<Bitmap?> DownloadWithTimeout(string url)
{
    Task<Bitmap> download = Stubs.GetBitmapAsync(url);
    if (download == await Task.WhenAny(download, Task.Delay(3000)))
    {
        return await download;
    }
    else
    {
        var ignored = download.ContinueWith(
            t => Trace($"Task finally completed: {t.Status}"));
        return null;
    }
}

static void Trace(string message) => Console.WriteLine(message);
Public Async Function DownloadWithTimeout(url As String) As Task(Of Bitmap)
    Dim download As Task(Of Bitmap) = Stubs.GetBitmapAsync(url)
    If download Is Await Task.WhenAny(download, Task.Delay(3000)) Then
        Return Await download
    Else
        Dim ignored = download.ContinueWith(Sub(t) TraceMsg($"Task finally completed: {t.Status}"))
        Return Nothing
    End If
End Function

Lo stesso principio si applica a più download, perché WhenAll restituisce un'attività:

public static async Task<Bitmap[]?> DownloadMultipleWithTimeout(string[] imageUrls)
{
    Task<Bitmap[]> downloads =
        Task.WhenAll(from url in imageUrls select Stubs.GetBitmapAsync(url));
    if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
    {
        return await downloads;
    }
    else
    {
        downloads.ContinueWith(t => Stubs.Log(t));
        return null;
    }
}
Public Async Function DownloadMultipleWithTimeout(imageUrls As String()) As Task(Of Bitmap())
    Dim downloads As Task(Of Bitmap()) =
        Task.WhenAll(From url In imageUrls Select Stubs.GetBitmapAsync(url))
    If downloads Is Await Task.WhenAny(downloads, Task.Delay(3000)) Then
        Return Await downloads
    Else
        downloads.ContinueWith(Sub(t) Stubs.Log(t))
        Return Nothing
    End If
End Function

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, si vuole ritentare un'operazione se un tentativo precedente ha esito negativo. Per il codice sincrono, è possibile compilare un metodo helper, RetryOnFault ad esempio nell'esempio seguente per eseguire questa attività:

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)!;
}
Public Function RetryOnFaultSync(Of T)(func As Func(Of T), maxTries As Integer) As T
    For i As Integer = 0 To maxTries - 1
        Try
            Return func()
        Catch
            If i = maxTries - 1 Then Throw
        End Try
    Next
    Return Nothing
End Function

È 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)!;
}
Public Async Function RetryOnFault(Of T)(func As Func(Of Task(Of T)), maxTries As Integer) As Task(Of T)
    For i As Integer = 0 To maxTries - 1
        Try
            Return Await func().ConfigureAwait(False)
        Catch
            If i = maxTries - 1 Then Throw
        End Try
    Next
    Return Nothing
End Function

È quindi possibile usare questo combinatore per codificare i ritentativi 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'altra Func<Task> richiamata dalla funzione tra tentativi per determinare quando ritentare l'operazione. Per esempio:

public static async Task<T> RetryOnFaultWithDelay<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)!;
}
Public Async Function RetryOnFaultWithDelay(Of T)(
    func As Func(Of Task(Of T)), maxTries As Integer, retryWhen As Func(Of Task)) As Task(Of T)
    For i As Integer = 0 To maxTries - 1
        Try
            Return Await func().ConfigureAwait(False)
        Catch
            If i = maxTries - 1 Then Throw
        End Try
        Await retryWhen().ConfigureAwait(False)
    Next
    Return Nothing
End Function

È quindi possibile usare la funzione come segue per attendere un secondo prima di ritentare 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 potrebbe 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<T>(
    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 => Stubs.Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return await completed;
}
Public Async Function NeedOnlyOne(Of T)(
    ParamArray functions As Func(Of CancellationToken, Task(Of T))()) As Task(Of T)
    Dim cts As New CancellationTokenSource()
    Dim tasks As Task(Of T)() = (From func In functions Select func(cts.Token)).ToArray()
    Dim completed As Task(Of T) = Await Task.WhenAny(tasks).ConfigureAwait(False)
    cts.Cancel()
    For Each task In tasks
        Dim ignored = task.ContinueWith(
            Sub(tsk) Stubs.Log(tsk), TaskContinuationOptions.OnlyOnFaulted)
    Next
    Return Await completed
End Function

È quindi possibile usare questa funzione come segue:

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

Operazioni intercalate

L'uso del metodo WhenAny per supportare uno scenario di interleaving può causare un problema di prestazioni quando si usano grandi set di attività. Ogni chiamata a WhenAny registra una continuazione con ogni attività. Per N numero di attività, questo processo crea continuazioni O(N2) per tutta la durata dell'operazione di interleaving. Se si usa un set di attività di grandi dimensioni, usare un combinatore (Interleaved nell'esempio seguente) per risolvere il problema di prestazioni:

public 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;
}
Public Function Interleaved(Of T)(tasks As IEnumerable(Of Task(Of T))) As IEnumerable(Of Task(Of T))
    Dim inputTasks As List(Of Task(Of T)) = tasks.ToList()
    Dim sources As List(Of TaskCompletionSource(Of T)) =
        (From _i In Enumerable.Range(0, inputTasks.Count) Select New TaskCompletionSource(Of T)()).ToList()
    Dim indexRef As Integer() = {-1}
    For Each inputTask In inputTasks
        inputTask.ContinueWith(Sub(completed)
                                   Dim idx = Interlocked.Increment(indexRef(0))
                                   Dim source = sources(idx)
                                   If completed.IsFaulted Then
                                       source.TrySetException(completed.Exception.InnerExceptions)
                                   ElseIf completed.IsCanceled Then
                                       source.TrySetCanceled()
                                   Else
                                       source.TrySetResult(completed.Result)
                                   End If
                               End Sub,
                               CancellationToken.None,
                               TaskContinuationOptions.ExecuteSynchronously,
                               TaskScheduler.Default)
    Next
    Return From source In sources Select source.Task
End Function

Usare il combinatore per elaborare i risultati delle attività man mano che vengono completate. Per esempio:

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

WhenAllOrFirstException

In alcuni scenari di scatter/gather, potrebbe essere necessario attendere tutte le attività in un set, a meno che si verifichi un errore. In tal caso, si vuole interrompere l'attesa non appena si verifica l'eccezione. È possibile eseguire questo comportamento usando 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 => ((Task<T>)t).Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}
Public Function WhenAllOrFirstException(Of T)(tasks As IEnumerable(Of Task(Of T))) As Task(Of T())
    Dim inputs As List(Of Task(Of T)) = tasks.ToList()
    Dim ce As New CountdownEvent(inputs.Count)
    Dim tcs As New TaskCompletionSource(Of T())()

    Dim onCompleted As Action(Of Task) = Sub(completed As Task)
                                             If completed.IsFaulted Then
                                                 tcs.TrySetException(completed.Exception.InnerExceptions)
                                             End If
                                             If ce.Signal() AndAlso Not tcs.Task.IsCompleted Then
                                                 tcs.TrySetResult(inputs.Select(Function(taskItem) DirectCast(taskItem, Task(Of T)).Result).ToArray())
                                             End If
                                         End Sub

    For Each t In inputs
        t.ContinueWith(onCompleted)
    Next
    Return tcs.Task
End Function

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 è possibile assegnarla a più consumatori. Tutti i consumatori possono attenderlo, registrare continuazioni, ottenere il risultato o le eccezioni (in caso di Task<TResult>) e così via. Questo aspetto 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> where TKey : notnull
{
    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(nameof(valueFactory));
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException(nameof(key));
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}
Public Class AsyncCache(Of TKey, TValue)
    Private ReadOnly _valueFactory As Func(Of TKey, Task(Of TValue))
    Private ReadOnly _map As New ConcurrentDictionary(Of TKey, Lazy(Of Task(Of TValue)))()

    Public Sub New(valueFactory As Func(Of TKey, Task(Of TValue)))
        If valueFactory Is Nothing Then Throw New ArgumentNullException(NameOf(valueFactory))
        _valueFactory = valueFactory
    End Sub

    Default Public ReadOnly Property Item(key As TKey) As Task(Of TValue)
        Get
            If key Is Nothing Then Throw New ArgumentNullException(NameOf(key))
            Return _map.GetOrAdd(key, Function(toAdd) New Lazy(Of Task(Of TValue))(Function() _valueFactory(toAdd))).Value
        End Get
    End Property
End Class

La classe AsyncCache<TKey,TValue> accetta come delegato al suo costruttore una funzione che prende un TKey e restituisce un Task<TResult>. Il dizionario interno archivia tutti i valori a cui si accede in precedenza dalla cache e AsyncCache garantisce che generi una sola attività per ogni 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.

static AsyncCache<string, string> m_webPages =
    new AsyncCache<string, string>(url => Stubs.DownloadStringTaskAsync(url));

public static async Task UseWebPageCache(string url)
{
    string contents = await m_webPages[url];
    Console.WriteLine(contents.Length);
}
Private m_webPages As New AsyncCache(Of String, String)(Function(url) Stubs.DownloadStringTaskAsync(url))

Public Async Function UseWebPageCache(url As String) As Task
    Dim contents As String = Await m_webPages(url)
    Console.WriteLine(contents.Length)
End Function

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 utilizzati dai consumatori, e i produttori e i consumatori possono operare 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, è sempre necessaria una struttura di dati per archiviare il lavoro creato dai producer 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;
            }
        }
    }
}
Public Class AsyncProducerConsumerCollection(Of T)
    Private ReadOnly m_collection As New Queue(Of T)()
    Private ReadOnly m_waiting As New Queue(Of TaskCompletionSource(Of T))()

    Public Sub Add(item As T)
        Dim tcs As TaskCompletionSource(Of T) = Nothing
        SyncLock m_collection
            If m_waiting.Count > 0 Then
                tcs = m_waiting.Dequeue()
            Else
                m_collection.Enqueue(item)
            End If
        End SyncLock
        If tcs IsNot Nothing Then tcs.TrySetResult(item)
    End Sub

    Public Function Take() As Task(Of T)
        SyncLock m_collection
            If m_collection.Count > 0 Then
                Return Task.FromResult(m_collection.Dequeue())
            Else
                Dim tcs As New TaskCompletionSource(Of T)()
                m_waiting.Enqueue(tcs)
                Return tcs.Task
            End If
        End SyncLock
    End Function
End Class

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

static AsyncProducerConsumerCollection<int> m_data = new();

public static async Task ConsumerAsync()
{
    while (true)
    {
        int nextItem = await m_data.Take();
        Stubs.ProcessNextItem(nextItem);
    }
}

public static void Produce(int data)
{
    m_data.Add(data);
}
Private m_data As New AsyncProducerConsumerCollection(Of Integer)()

Public Async Function ConsumerAsync() As Task
    While True
        Dim nextItem As Integer = Await m_data.Take()
        Stubs.ProcessNextItem(nextItem)
    End While
End Function

Public Sub Produce(data As Integer)
    m_data.Add(data)
End Sub

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.

static BufferBlock<int> m_dataBlock = new();

public static async Task ConsumerAsyncBlock()
{
    while (true)
    {
        int nextItem = await m_dataBlock.ReceiveAsync();
        Stubs.ProcessNextItem(nextItem);
    }
}

public static void ProduceBlock(int data)
{
    m_dataBlock.Post(data);
}
Private m_dataBlock As New BufferBlock(Of Integer)()

Public Async Function ConsumerAsyncBlock() As Task
    While True
        Dim nextItem As Integer = Await m_dataBlock.ReceiveAsync()
        Stubs.ProcessNextItem(nextItem)
    End While
End Function

Public Sub ProduceBlock(data As Integer)
    m_dataBlock.Post(data)
End Sub

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