Använda det aktivitetsbaserade asynkrona mönstret

När du använder det uppgiftsbaserade asynkrona mönstret (TAP) för att arbeta med asynkrona operationer kan du använda återanrop för att uppnå väntan utan att blockera. För uppgifter använder det här mönstret metoder som Task.ContinueWith. Språkbaserat asynkront stöd döljer återanrop genom att tillåta att asynkrona åtgärder väntar i det normala kontrollflödet, och kompilatorgenererad kod ger samma stöd på API-nivå.

Avbryta programkörning med Await

Du kan använda nyckelordet await i C# och Await Operator i Visual Basic för att asynkront vänta Task och Task<TResult> objekt. När du väntar på en Taskawait är uttrycket av typen void. När du väntar på en Task<TResult>await är uttrycket av typen TResult. Ett await uttryck måste förekomma i en asynkron metods brödtext. (Dessa språkfunktioner introducerades i .NET Framework 4.5.)

I bakgrunden installerar await-funktionen ett återanrop på uppgiften med hjälp av en fortsättningsoperation. Återanropet återupptar den asynkrona metoden vid tidpunkten för avstängningen. När den asynkrona metoden återupptas, om den väntade operationen har slutförts framgångsrikt och var av typen Task<TResult>, returneras dess TResult. Om Task eller Task<TResult> som väntades avslutades i Canceled-tillståndet, utlöses ett OperationCanceledException-undantag. Om det Task eller Task<TResult> som väntades slutade i Faulted-tillståndet kastas det undantag som orsakade felet. En Task kan gå fel som ett resultat av flera undantag, men endast ett av dessa undantag sprids. Egenskapen returnerar dock Task.Exception ett AggregateException undantag som innehåller alla fel.

Om en synkroniseringskontext (SynchronizationContext objekt) är associerad med tråden som körde den asynkrona metoden vid tidpunkten för avstängningen (till exempel om SynchronizationContext.Current egenskapen inte nullär ), återupptas den asynkrona metoden i samma synkroniseringskontext med hjälp av kontextens Post metod. Annars förlitar den sig på schemaläggaren (TaskScheduler objektet) som var aktuell vid tidpunkten för avbrottet. Vanligtvis är detta standardschemaläggaren (TaskScheduler.Default), som riktar sig mot trådpoolen. Den här schemaläggaren avgör om den förväntade asynkrona åtgärden ska återupptas där den slutfördes eller om återupptagandet ska schemaläggas. Standardschemaläggaren tillåter vanligtvis att fortsättningen körs på den tråd där den väntade åtgärden blev slutförd.

När du anropar en asynkron metod körs funktionens brödtext synkront fram till det första inväntningsuttrycket på en väntande instans som ännu inte är klar. Då återgår anropet till anroparen. Om den asynkrona metoden inte returnerar void, returnerar den ett Task- eller Task<TResult>-objekt för att representera den aktuella beräkningen. Om en retursats påträffas eller slutet av metodtexten nås i en icke-void-asynkron metod slutförs uppgiften i det RanToCompletion slutliga tillståndet. Om ett ohanterat undantag gör att kontrollen lämnar den asynkrona metodens kropp, avslutas uppgiften i tillståndet Faulted. Om undantaget är ett OperationCanceledExceptionslutar aktiviteten i stället i Canceled tillståndet . På så sätt publiceras resultatet eller undantaget så småningom.

Det finns flera viktiga varianter av det här beteendet. Av prestandaskäl, om en uppgift redan är klar vid den tidpunkt då den väntas på, lämnas inte kontrollen över och funktionen fortsätter att köras. Att återgå till den ursprungliga kontexten är dessutom inte alltid det önskade beteendet och kan ändras. det här beteendet beskrivs mer detaljerat i nästa avsnitt.

Konfigurera avstängning och återupptagning med Yield och ConfigureAwait

Flera metoder ger mer kontroll över körningen av en asynkron metod. Du kan till exempel använda Task.Yield metoden för att introducera en avkastningspunkt i den asynkrona metoden:

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

Den här metoden motsvarar att asynkront publicera eller schemalägga tillbaka till den aktuella kontexten.

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

Du kan också använda metoden Task.ConfigureAwait för bättre kontroll över paus och återupptagande i en asynkron metod. Som tidigare nämnts avbildas den aktuella kontexten som standard när en asynkron metod pausas och den insamlade kontexten används för att anropa den asynkrona metodens fortsättning vid återupptagande. I många fall är detta det exakta beteendet du vill ha. I andra fall kanske du inte bryr dig om fortsättningskontexten, och du kan uppnå bättre prestanda genom att undvika sådana inlägg tillbaka till den ursprungliga kontexten. Om du vill aktivera det här beteendet, använd metoden Task.ConfigureAwait för att informera väntoperationen att inte fånga och återuppta kontexten, utan att fortsätta körningen där den asynkrona åtgärden slutfördes.

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Awaitables, ConfigureAwait och SynchronizationContext

await fungerar med alla typer som uppfyller det väntande uttrycksmönstret, inte bara Task. En typ är väntande om den tillhandahåller en kompatibel GetAwaiter metod som returnerar en typ med IsCompleted, OnCompletedoch GetResult medlemmar. I de flesta offentliga API:er returnerar du Task, Task<TResult>, ValueTaskeller ValueTask<TResult>. Använd endast anpassade awaitables för specialiserade scenarier.

Använd ConfigureAwait när fortsättningen inte behöver anroparens kontext. I appkod som uppdaterar ett användargränssnitt krävs ofta kontextinsamling. I återanvändbar bibliotekskod ConfigureAwait(false) föredras vanligtvis eftersom det undviker onödiga kontexthopp och minskar risken för dödläge för anropare som blockerar.

ConfigureAwait(false) ändrar fortsättningsschemaläggning, inte ExecutionContext flöde. En djupare förklaring av kontextbeteende finns i ExecutionContext och SynchronizationContext.

Avbryta en asynkron åtgärd

Från och med .NET Framework 4 ger TAP-metoder som stöder annullering minst en överlagring som accepterar en annulleringstoken (CancellationToken -objekt).

Du skapar en annulleringstoken via en källa för annulleringstoken (CancellationTokenSource objekt). Källans Token egenskap returnerar annulleringstoket som signalerar när källans Cancel metod anropas.

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

Om du till exempel vill ladda ned en enda webbsida och vill kunna avbryta åtgärden skapar du ett CancellationTokenSource objekt, skickar dess token till TAP-metoden och anropar sedan källans Cancel metod när du är redo att avbryta åtgärden:

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();

Eller så kan du skicka samma token till en selektiv delmängd av åtgärder:

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();

Viktigt!

Alla trådar kan initiera annulleringsbegäranden.

Du kan skicka CancellationToken.None värdet till vilken metod som helst som accepterar en annulleringstoken för att ange att annullering aldrig begärs. Det här värdet gör att egenskapen CancellationToken.CanBeCanceled returnerar false, och den anropade metoden kan optimeras därefter. För testningsändamål kan du också skicka in en förinställd annulleringstoken som instansieras med hjälp av konstruktorn som accepterar ett booleskt värde för att ange om token ska starta i ett redan avbrutet eller inte avbrutet tillstånd.

Den här metoden för annullering har flera fördelar:

  • Du kan skicka samma annulleringstoken till valfritt antal asynkrona och synkrona åtgärder.

  • Samma begäran om annullering kan gå till valfritt antal lyssnare.

  • Utvecklaren av det asynkrona API:et har fullständig kontroll över om annullering kan begäras och när det börjar gälla.

  • Koden som använder API:et kan selektivt fastställa de asynkrona anrop som annulleringsbegäranden går till.

Övervaka förlopp

Vissa asynkrona metoder exponerar förloppet via ett förloppsgränssnitt som du skickar till den asynkrona metoden. Tänk dig till exempel en funktion som asynkront laddar ned en textsträng och på vägen genererar förloppsuppdateringar som inkluderar procentandelen av nedladdningen som har slutförts hittills. Du kan använda en sådan metod i ett Windows Presentation Foundation-program (WPF) på följande sätt:

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; }
}

Använda de inbyggda aktivitetsbaserade kombinatorerna

Namnområdet System.Threading.Tasks innehåller flera metoder för att skapa och arbeta med uppgifter.

Anmärkning

Flera kodexempel i det här avsnittet använder Bitmap, vilket kräver paketet System.Drawing.Common och stöds endast på Windows. De asynkrona mönster som de demonstrerar gäller på alla plattformar. Använd ett plattformsoberoende bildbibliotek för mål som inte är Windows.

Uppgift.Kör

Klassen Task innehåller flera Run metoder som gör att du enkelt kan avlasta arbete som en Task eller Task<TResult> till trådpoolen. Som exempel:

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

Vissa av dessa Run metoder, till exempel överlagringen Task.Run(Func<Task>) , finns som förkortning för TaskFactory.StartNew metoden. Med den här överlagringen kan du använda await i det avlastade arbetet. Som exempel:

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

Sådana överbelastningar är logiskt likvärdiga med att använda TaskFactory.StartNew-metoden tillsammans med Unwrap-tilläggsmetoden i biblioteket för parallella uppgifter.

Task.FromResult

FromResult Använd metoden i scenarier där data kanske redan är tillgängliga och du bara behöver returnera dem från en uppgiftsreturmetod som lyfts till en 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

WhenAll Använd metoden för att asynkront vänta på flera asynkrona åtgärder som representeras som uppgifter. Metoden har flera överbelastningar som stöder en uppsättning icke-generiska uppgifter eller en icke-enhetlig uppsättning generiska uppgifter (till exempel asynkront väntande på flera åtgärder som inte returnerar något värde, eller asynkront väntande på flera metoder för värderetur där varje värde kan ha en annan typ) och för att stödja en enhetlig uppsättning generiska uppgifter (till exempel asynkront väntande på flera TResult-returnerande metoder).

Anta att du vill skicka e-postmeddelanden till flera kunder. Du kan överlappa att skicka meddelandena så att du inte väntar på att ett meddelande ska slutföras innan du skickar nästa. Du kan också ta reda på när sändningsåtgärderna har slutförts och om några fel inträffar:

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

Den här koden hanterar inte undantag uttryckligen som kan inträffa, men den låter undantag överföras från await till resultataktiviteten från WhenAll. Om du vill hantera undantagen använder du kod som följande:

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

I det här fallet, om någon asynkron åtgärd misslyckas, konsolideras alla undantag i ett AggregateException undantag, som lagras i Task som returneras från WhenAll metoden. Endast ett av dessa undantag sprids dock av nyckelordet await . Om du vill undersöka alla undantag kan du skriva om den tidigare koden på följande sätt:

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

Överväg ett exempel på hur du laddar ned flera filer från webben asynkront. I det här fallet har alla asynkrona åtgärder homogena resultattyper och det är enkelt att komma åt resultaten:

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

Du kan använda samma metoder för undantagshantering som beskrivs i det tidigare scenariot med 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

WhenAny Använd metoden för att asynkront vänta på att bara en av flera asynkrona åtgärder som representeras som uppgifter ska slutföras. Den här metoden hanterar fyra primära användningsfall:

  • Redundans: Utför en åtgärd flera gånger och väljer den som slutförs först (till exempel genom att kontakta flera webbtjänster för aktieofferter som returnerar ett enda resultat och väljer den som slutförs snabbast).

  • Interleaving: Starta flera åtgärder och vänta tills alla är slutförda, men bearbeta dem så snart de slutförs.

  • Begränsning: Gör att ytterligare åtgärder kan börja när andra slutförs. Det här scenariot är en förlängning av interleaving-scenariot.

  • Tidig räddningsinsats: En uppgift, som t1, kan till exempel grupperas med en annan uppgift, t2, i en WhenAny uppgift och du kan vänta på WhenAny uppgiften. Uppgift t2 kan representera en timeout eller annullering eller någon annan signal som gör WhenAny att aktiviteten slutförs innan t1 slutförs.

Redundans

Överväg ett fall där du vill fatta ett beslut om du vill köpa en aktie. Det finns flera webbtjänster för aktierekommendationer som du litar på, men beroende på den dagliga belastningen kan varje tjänst bli långsam vid olika tidpunkter. WhenAny Använd metoden för att ta emot ett meddelande när en åtgärd slutförs:

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

Till skillnad från WhenAll, som returnerar de oöppnade resultaten för alla aktiviteter som har slutförts, WhenAny returnerar den uppgift som har slutförts. Om en aktivitet misslyckas är det viktigt att veta att den misslyckades, och om en aktivitet lyckas är det viktigt att veta vilken uppgift som returvärdet är associerat med. Därför måste du komma åt resultatet av den returnerade aktiviteten, eller invänta den ytterligare, som det här exemplet visar.

Precis som med WhenAllmåste du kunna hantera undantag. Eftersom du får tillbaka den slutförda uppgiften, kan du invänta att den returnerade uppgiften sprider felen, och try/catch dem passande; till exempel:

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

Även om en första uppgift slutförs kan efterföljande aktiviteter dessutom misslyckas. I det här läget har du flera alternativ för att hantera undantag: Du kan vänta tills alla startuppgifter har slutförts, i vilket fall du kan använda WhenAll metoden, eller så kan du bestämma att alla undantag är viktiga och måste loggas. I det här scenariot kan du använda fortsättningar för att få ett meddelande när aktiviteter slutförs asynkront:

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

eller:

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

eller till och med:

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

Slutligen kanske du vill avbryta alla återstående åtgärder:

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

Interfoliering

Tänk dig ett fall där du laddar ned bilder från webben och bearbetar varje bild (till exempel lägga till bilden i en användargränssnittskontroll). Du bearbetar bilderna sekventiellt i användargränssnittstråden, men vill ladda ned bilderna så samtidigt som möjligt. Du vill inte heller lägga till bilderna i användargränssnittet förrän alla laddas ned. I stället vill du lägga till dem när de har slutförts.

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

Du kan också tillämpa interleaving på ett scenario som omfattar beräkningsintensiv bearbetning på de ThreadPool nedladdade bilderna, till exempel:

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

Strypning

Överväg interleaving-exemplet, förutom att användaren laddar ned så många bilder att nedladdningarna måste begränsas. Du vill till exempel bara att ett visst antal nedladdningar ska ske samtidigt. För att uppnå det här målet startar du en delmängd av de asynkrona åtgärderna. När operationerna har slutförts kan du starta ytterligare operationer för att ta deras plats.

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

Tidigt ekonomiskt stöd

Tänk på att du väntar asynkront på att en åtgärd ska slutföras samtidigt som du svarar på en användares annulleringsbegäran (till exempel klickade användaren på en avbryt-knapp). Följande kod illustrerar det här scenariot:

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

Den här implementeringen återaktiverar användargränssnittet så snart du bestämmer dig för att avbryta, men avbryter inte de underliggande asynkrona operationerna. Ett annat alternativ är att avbryta väntande åtgärder när du bestämmer dig för att lösa ut, men inte återupprätta användargränssnittet förrän åtgärderna har slutförts, eventuellt på grund av att de slutar tidigt på grund av annulleringsbegäran:

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

Ett annat exempel på tidig räddningsaktion är att använda WhenAny metoden tillsammans med Delay metoden, enligt beskrivningen i nästa avsnitt.

Task.Delay

Task.Delay Använd metoden för att lägga till pauser i körningen av en asynkron metod. Den här pausen är användbar för många typer av funktioner, inklusive att skapa pollningsslingor och fördröja hanteringen av användarinmatning under en förutbestämd tidsperiod. Du kan också använda metoden Task.Delay med Task.WhenAny för att implementera tidsgränser på väntan.

Om en aktivitet som ingår i en större asynkron åtgärd (till exempel en ASP.NET webbtjänst) tar för lång tid att slutföra kan den övergripande åtgärden bli lidande, särskilt om den inte kan slutföras. Av denna anledning är det viktigt att kunna ställa in en tidsgräns när du väntar på en asynkron åtgärd. De synkrona Task.Waitmetoderna , Task.WaitAlloch Task.WaitAny accepterar timeout-värden, men motsvarandeTaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAnyoch de tidigare nämnda Task.WhenAll/Task.WhenAny metoderna gör det inte. Använd Task.Delay i stället och Task.WhenAny tillsammans för att implementera en tidsgräns.

Anta till exempel i ditt användargränssnittsprogram att du vill ladda ned en bild och inaktivera användargränssnittet medan avbildningen laddas ned. Men om nedladdningen tar för lång tid vill du återaktivera användargränssnittet och ignorera nedladdningen:

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

Samma princip gäller för flera nedladdningar eftersom WhenAll returnerar en uppgift:

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

Skapa aktivitetsbaserade kombinatorer

Eftersom en uppgift kan representera en asynkron åtgärd helt och hållet och tillhandahålla synkrona och asynkrona funktioner för att ansluta till åtgärden, hämta dess resultat och så vidare, kan du skapa användbara bibliotek med kombinatorer som skapar uppgifter för att skapa större mönster. Som beskrivs i föregående avsnitt innehåller .NET flera inbyggda kombinatorer, men du kan också skapa egna. Följande avsnitt innehåller flera exempel på potentiella kombinatormetoder och typer.

Försök igen vid fel

I många situationer vill du försöka utföra en åtgärd igen om ett tidigare försök misslyckas. För synkron kod kan du skapa en hjälpmetod som RetryOnFault i följande exempel för att utföra den här uppgiften:

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

Du kan skapa en nästan identisk hjälpmetod för asynkrona åtgärder som implementeras med TAP och därmed returnera uppgifter:

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

Du kan sedan använda den här kombinatorn för att koda återförsök till programmets logik. Som exempel:

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

Du kan utöka RetryOnFault funktionen ytterligare. Funktionen kan till exempel acceptera en annan Func<Task> som anropas mellan återförsök för att avgöra när operationen ska försöka igen. Som exempel:

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

Du kan sedan använda funktionen på följande sätt för att vänta en sekund innan du försöker utföra åtgärden igen:

// 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

Ibland kan du dra nytta av redundans för att förbättra en åtgärds svarstid och chanser att lyckas. Överväg flera webbtjänster som tillhandahåller aktiekurser, men vid olika tidpunkter på dagen kan varje tjänst ge olika nivåer av kvalitet och svarstider. För att hantera dessa fluktuationer kan du utfärda begäranden till alla webbtjänster, och så snart du får ett svar från en av dem avbryter du de återstående begärandena. Du kan implementera en hjälpfunktion för att göra det enklare att implementera det här vanliga mönstret för att starta flera åtgärder, vänta på alla och sedan avbryta resten. Funktionen NeedOnlyOne i följande exempel illustrerar det här scenariot:

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

Du kan sedan använda den här funktionen på följande sätt:

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

Interleaverade operationer

Användningen av metoden WhenAny för att stödja ett interfolieringsscenario kan orsaka prestandaproblem när du arbetar med stora mängder uppgifter. Varje anrop till WhenAny registrerar en fortsättning för varje uppgift. För N antal uppgifter skapar den här processen O(N2) fortsättningar under livslängden för interfolieringsåtgärden. Om du arbetar med en stor uppsättning uppgifter använder du en kombinator (Interleaved i följande exempel) för att åtgärda prestandaproblemet:

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

Använd kombinatorn för att bearbeta resultatet av aktiviteter när de slutförs. Som exempel:

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

WhenAllOrFirstException

I vissa scatter/gather-scenarier kanske du vill vänta på att alla uppgifter (tasks) i en uppsättning ska slutföras, såvida inte någon av dem resulterar i ett fel. I så fall vill du sluta vänta så snart undantaget inträffar. Du kan utföra det beteendet med hjälp av en combinatormetod, till exempel WhenAllOrFirstException i följande exempel:

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

Skapa uppgiftsbaserade datastrukturer

Förutom möjligheten att skapa anpassade uppgiftsbaserade kombinatorer gör en datastruktur i Task och Task<TResult> som representerar både resultatet av en asynkron åtgärd och den synkronisering som krävs för att ansluta till den en kraftfull typ som du kan skapa anpassade datastrukturer som ska användas i asynkrona scenarier.

AsyncCache

En viktig aspekt av en uppgift är att du kan dela ut den till flera konsumenter. Alla konsumenter kan vänta på det, registrera fortsättningar med det, få sina resultat eller undantag (i fallet med Task<TResult>), och så vidare. Den här aspekten gör Task och Task<TResult> perfekt lämpade för att användas i en asynkron cachelagringsinfrastruktur. Här är ett exempel på en liten men kraftfull asynkron cache som bygger på 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

Klassen AsyncCache<TKey,TValue> accepterar som ett ombud till konstruktorn en funktion som tar en TKey och returnerar en Task<TResult>. Den interna ordlistan lagrar alla värden som använts tidigare från cacheminnet och AsyncCache säkerställer att den endast genererar en uppgift per nyckel, även om cachen används samtidigt.

Du kan till exempel skapa en cache för nedladdade webbsidor:

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

Du kan sedan använda den här cachen i asynkrona metoder när du behöver innehållet på en webbsida. Klassen AsyncCache ser till att du laddar ned så få sidor som möjligt och cachelagrar resultatet.

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

Du kan också använda uppgifter för att skapa datastrukturer för att samordna asynkrona aktiviteter. Överväg ett av de klassiska parallella designmönstren: producent/konsument. I det här mönstret genererar producenterna data som konsumenterna förbrukar, och producenterna och konsumenterna kan köras parallellt. Till exempel bearbetar konsumenten objekt 1, som tidigare genererades av en producent som nu producerar objekt 2. För producent-/konsumentmönstret behöver du alltid viss datastruktur för att lagra det arbete som skapas av producenter så att konsumenterna kan meddelas om nya data och hitta dem när de är tillgängliga.

Här är en enkel datastruktur, byggd ovanpå uppgifter, vilket gör att asynkrona metoder kan användas som producenter och konsumenter.

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

Med datastrukturen på plats kan du skriva kod, till exempel följande:

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

Namnområdet System.Threading.Tasks.Dataflow innehåller den BufferBlock<T> typ som du kan använda på ett liknande sätt, men utan att behöva skapa en anpassad samlingstyp:

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

Anmärkning

Namnområdet System.Threading.Tasks.Dataflow är tillgängligt som ett NuGet-paket. Om du vill installera sammansättningen som innehåller System.Threading.Tasks.Dataflow namnområdet öppnar du projektet i Visual Studio, väljer Hantera NuGet-paket på Project-menyn och söker online efter System.Threading.Tasks.Dataflow paketet.

Se även