Het Asynchrone taakgebaseerde patroon gebruiken

Wanneer u het op taken gebaseerde Asynchrone patroon (TAP) gebruikt om te werken met asynchrone bewerkingen, kunt u callbacks gebruiken om te wachten zonder te blokkeren. Voor taken gebruikt dit patroon methoden zoals Task.ContinueWith. Taalgebaseerde asynchrone ondersteuning verbergt callbacks door het mogelijk te maken dat op asynchrone bewerkingen binnen de normale controleflow kan worden gewacht, en door door de compiler gegenereerde code wordt dezelfde ondersteuning op API-niveau geboden.

Onderbreken van uitvoering met Await

U kunt het trefwoord await in C# en de Await Operator in Visual Basic gebruiken om asynchroon te wachten Task en Task<TResult> objecten. Wanneer u wacht op een Task, is de await expressie van het type void. Wanneer u wacht op een Task<TResult>, is de await expressie van het type TResult. Een await expressie moet voorkomen in de hoofdtekst van een asynchrone methode. (Deze taalfuncties zijn geïntroduceerd in .NET Framework 4.5.)

Achter de schermen installeert de await functionaliteit een callback voor de taak door middel van een voortzetting. Deze callback hervat de asynchrone methode op het punt van onderbreking. Wanneer de asynchrone methode wordt hervat, en de wachtende bewerking met succes is voltooid en een Task<TResult> was, dan wordt de TResult geretourneerd. Als de Task of Task<TResult> die werd afgewacht, in de Canceled status beëindigd is, wordt er een OperationCanceledException uitzondering opgeworpen. Als de Task of Task<TResult> waarop werd gewacht en die eindigde in de Faulted toestand, wordt de uitzondering die de fout veroorzaakte opgeworpen. Een Task fout kan optreden als gevolg van meerdere uitzonderingen, maar er wordt slechts één van deze uitzonderingen doorgegeven. De Task.Exception eigenschap retourneert echter een AggregateException uitzondering die alle fouten bevat.

Als een synchronisatiecontext (SynchronizationContext object) is gekoppeld aan de thread die de asynchrone methode uitvoerde op het moment van opschorten (bijvoorbeeld als de SynchronizationContext.Current eigenschap niet nullis), wordt de asynchrone methode hervat op dezelfde synchronisatiecontext met behulp van de methode van Post de context. Anders is het afhankelijk van de taakplanner (TaskScheduler object) die op het moment van schorsing actueel was. Dit is doorgaans de standaardtaakplanner (TaskScheduler.Default), die is gericht op de threadpool. Deze taakplanner bepaalt of de verwachte asynchrone bewerking moet worden hervat waar deze is voltooid of of de hervatting moet worden gepland. Met de standaardplanner kan de voortzetting doorgaans worden uitgevoerd op de thread waar de afgewachte operatie is voltooid.

Wanneer u een asynchrone methode aanroept, wordt de hoofdtekst van de functie synchroon uitgevoerd totdat de eerste await-expressie wordt bereikt op een instantie waarop gewacht moet worden die nog niet voltooid is, waarna de aanroep terugkeert naar de aanroeper. Als de asynchrone methode geen void retourneert, retourneert deze een Task of Task<TResult> object om de lopende berekening weer te geven. Als in een niet-void asynchrone methode een return-opdracht wordt aangetroffen of het einde van de methodebody wordt bereikt, wordt de taak voltooid in de RanToCompletion uiteindelijke toestand. Als een niet-verwerkte uitzondering ervoor zorgt dat de besturing de hoofdtekst van de asynchrone methode verlaat, eindigt de taak in de Faulted toestand. Als deze uitzondering een OperationCanceledExceptionuitzondering is, eindigt de taak in plaats daarvan in de Canceled status. Op deze manier wordt het resultaat of de uitzondering uiteindelijk gepubliceerd.

Er bestaan verschillende belangrijke variaties van dit gedrag. Als een taak al is voltooid op het moment dat de taak wordt afgewacht, wordt de besturing niet overgedragen en gaat de functie door. Daarnaast is terugkeren naar de oorspronkelijke context niet altijd het gewenste gedrag en kan worden gewijzigd; dit gedrag wordt uitgebreid beschreven in de volgende sectie.

Configureren van onderbreking en hervatting met Yield en ConfigureAwait

Verschillende methoden bieden meer controle over de uitvoering van een asynchrone methode. U kunt bijvoorbeeld de Task.Yield methode gebruiken om een rendementspunt te introduceren in de asynchrone methode:

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

Deze methode is gelijk aan asynchroon posten of plannen naar de huidige context.

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

U kunt de Task.ConfigureAwait methode ook gebruiken voor betere controle over schorsing en hervatting in een asynchrone methode. Zoals eerder vermeld, wordt de huidige context standaard vastgelegd op het moment dat een asynchrone methode wordt onderbroken en die vastgelegde context wordt gebruikt om de voortzetting van de asynchrone methode aan te roepen bij hervatting. In veel gevallen is dit het exacte gedrag dat u wilt. In andere gevallen geeft u mogelijk niet om de vervolgcontext en kunt u betere prestaties bereiken door dergelijke berichten terug te zetten naar de oorspronkelijke context. Als u dit gedrag wilt inschakelen, gebruikt u de Task.ConfigureAwait-methode om aan te geven dat de wachtbewerking niet vastgelegd en hervat moet worden op de context, maar dat de uitvoering moet doorgaan op de plek waar de asynchrone bewerking die werd afgewacht is voltooid.

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Awaitables, ConfigureAwait en SynchronizationContext

await werkt met elk type dat voldoet aan het te verwachten expressiepatroon, niet alleen Task. Een type kan worden verwacht als het een compatibele GetAwaiter methode biedt die een type retourneert met IsCompleted, OnCompleteden GetResult leden. In de meeste openbare API's retourneert Task, Task<TResult>, ValueTask of ValueTask<TResult>. Gebruik aangepaste wachtobjecten alleen voor gespecialiseerde scenario's.

Gebruik ConfigureAwait deze optie wanneer de voortzetting de context van de aanroeper niet nodig heeft. In app-code waarmee een gebruikersinterface wordt bijgewerkt, is het vastleggen van context vaak vereist. In herbruikbare bibliotheekcode heeft ConfigureAwait(false) meestal de voorkeur omdat hiermee onnodige contexthops worden vermeden en het risico op deadlock voor oproepen die blokkeren vermindert.

ConfigureAwait(false) wijzigt de doorgaanplanning, niet de ExecutionContext stroom. Zie ExecutionContext en SynchronizationContext voor een diepere uitleg van contextgedrag.

Een asynchrone bewerking annuleren

Vanaf .NET Framework 4 bieden TAP-methoden die ondersteuning bieden voor annulering ten minste één overbelasting die een annuleringstoken (CancellationToken object) accepteert.

U maakt een annuleringstoken via een annuleringstokenbron (CancellationTokenSource object). De eigenschap Token retourneert het annuleringstoken dat aangeeft wanneer de methode Cancel van de bron wordt aangeroepen.

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

Als u bijvoorbeeld één webpagina wilt downloaden en u de bewerking wilt annuleren, maakt u een CancellationTokenSource object, geeft u het token door aan de TAP-methode en roept u de methode van Cancel de bron aan wanneer u klaar bent om de bewerking te annuleren:

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

U kunt hetzelfde token ook doorgeven aan een selectieve subset van bewerkingen:

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

Belangrijk

Elke thread kan annuleringsaanvragen initiëren.

U kunt de CancellationToken.None waarde doorgeven aan elke methode die een annuleringstoken accepteert om aan te geven dat annulering nooit wordt aangevraagd. Deze waarde zorgt ervoor dat de CancellationToken.CanBeCanceled eigenschap wordt geretourneerd falseen dat de aangeroepen methode dienovereenkomstig kan worden geoptimaliseerd. Voor testdoeleinden kunt u ook een vooraf geannuleerd annuleringstoken doorgeven dat wordt geïnstantieerd met behulp van de constructor die een Boole-waarde accepteert om aan te geven of het token moet beginnen met een al geannuleerde of niet-geannuleerde status.

Deze benadering van annulering heeft verschillende voordelen:

  • U kunt hetzelfde annuleringstoken doorgeven aan een willekeurig aantal asynchrone en synchrone bewerkingen.

  • Dezelfde annuleringsaanvraag kan naar een willekeurig aantal listeners gaan.

  • De ontwikkelaar van de asynchrone API heeft volledige controle over of annulering kan worden aangevraagd en wanneer deze van kracht wordt.

  • De code waarmee de API wordt gebruikt, kan selectief de asynchrone aanroepen bepalen waarnaar annuleringsaanvragen worden verzonden.

Voortgang bewaken

Sommige asynchrone methoden maken voortgang zichtbaar via een voortgangsinterface die u doorgeeft aan de asynchrone methode. Denk bijvoorbeeld aan een functie waarmee asynchroon een tekenreeks met tekst wordt gedownload. Daarnaast worden voortgangsupdates gegenereerd die het percentage van de download bevatten dat tot nu toe is voltooid. U kunt op de volgende manier gebruik maken van een dergelijke methode in een Windows Presentation Foundation (WPF)-applicatie:

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

De ingebouwde taakgebaseerde combinatoren gebruiken

De System.Threading.Tasks naamruimte bevat verschillende methoden voor het opstellen en werken met taken.

Opmerking

In verschillende codevoorbeelden in deze sectie wordt gebruikgemaakt van Bitmap, waarvoor het pakket System.Drawing.Common is vereist en alleen wordt ondersteund op Windows. De asynchrone patronen die ze demonstreren, zijn van toepassing op alle platforms; vervang een platformoverschrijdende imagingbibliotheek voor niet-Windows doelen.

Task.Run

De Task klasse bevat verschillende Run methoden waarmee u eenvoudig werk kunt uitbesteden als een Task of Task<TResult> naar de threadpool. Voorbeeld:

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

Sommige van deze Run methoden, zoals de Task.Run(Func<Task>) overbelasting, bestaan als afkorting voor de TaskFactory.StartNew methode. Met deze overbelaste functie kunt u await binnen het uit te voeren werk gebruiken. Voorbeeld:

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

Dergelijke overbelastingen zijn logisch gelijk aan het gebruik van de TaskFactory.StartNew methode in combinatie met de Unwrap extensiemethode in de taakparallelbibliotheek.

Task.FromResult

Gebruik de FromResult-methode in scenario's waarin gegevens mogelijk al beschikbaar zijn en u deze eenvoudigweg vanuit een taak-terugkerende methode dient te retourneren die is geïntegreerd in een 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

Gebruik de WhenAll methode om asynchroon te wachten op meerdere asynchrone bewerkingen die worden weergegeven als taken. De methode heeft meerdere overbelastingen die ondersteuning bieden voor een set van niet-generieke taken of een niet-uniforme set van generieke taken (bijvoorbeeld asynchroon wachten op meerdere void-retournerende bewerkingen, of asynchroon wachten op meerdere waarderetterende methoden waarbij elke waarde mogelijk een ander type heeft) en om een uniforme set van generieke taken te ondersteunen (zoals asynchroon wachten op meerdere TResult-retournerende methoden).

Stel dat u e-mailberichten naar verschillende klanten wilt verzenden. U kunt het verzenden van de berichten overlappen, zodat u niet wacht tot één bericht is voltooid voordat u het volgende verzendt. U kunt ook achterhalen wanneer de verzendbewerkingen zijn voltooid en of er fouten optreden:

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

Deze code verwerkt uitzonderingen die zich kunnen voordoen niet expliciet, maar laat uitzonderingen vanuit de await doorgaan naar de resulterende taak van WhenAll. Als u de uitzonderingen wilt afhandelen, gebruikt u code zoals de volgende:

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 dit geval, als een asynchrone bewerking mislukt, worden alle uitzonderingen samengevoegd in een AggregateException uitzondering, die wordt opgeslagen in de Task bewerking die wordt geretourneerd door de WhenAll methode. Er wordt echter slechts één van deze uitzonderingen doorgegeven door het await trefwoord. Als u alle uitzonderingen wilt onderzoeken, kunt u de vorige code als volgt herschrijven:

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

Bekijk een voorbeeld van het asynchroon downloaden van meerdere bestanden van het web. In dit geval hebben alle asynchrone bewerkingen homogene resultaattypen en is het eenvoudig om toegang te krijgen tot de resultaten:

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

U kunt dezelfde technieken voor uitzonderingsafhandeling gebruiken zoals besproken in het vorige void-retournerende scenario.

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

Gebruik de WhenAny methode om asynchroon te wachten op slechts één van meerdere asynchrone bewerkingen die worden weergegeven als taken die moeten worden voltooid. Deze methode dient voor vier primaire use cases:

  • Redundantie: een bewerking meerdere keren uitvoeren en de bewerking selecteren die het eerst wordt voltooid (bijvoorbeeld contact opnemen met meerdere webservices voor aandelenkoersen die één resultaat retourneren en de bewerking selecteren die het snelst wordt voltooid).

  • Interleaving: Meerdere bewerkingen gelijktijdig starten en wachten tot ze allemaal voltooid zijn, terwijl ze worden verwerkt zodra ze klaar zijn.

  • Snelheidsbeperking: aanvullende operaties toestaan om te beginnen als anderen zijn voltooid. Dit scenario is een uitbreiding van het interleavingsscenario.

  • Vroege borgtocht: Een bewerking die wordt vertegenwoordigd door taak t1, kan bijvoorbeeld worden gegroepeerd in een WhenAny taak met een andere taak t2 en u kunt wachten op de WhenAny taak. Taak t2 kan een time-out of annulering vertegenwoordigen, of een ander signaal dat ervoor zorgt dat de WhenAny taak wordt voltooid voordat t1 is voltooid.

Redundantie

Overweeg een geval waarin u een beslissing wilt nemen over het kopen van een aandelen. Er bestaan verschillende webservices voor aanbevelingen voor aandelen die u vertrouwt, maar afhankelijk van de dagelijkse belasting kan elke service op verschillende momenten traag zijn. Gebruik de WhenAny methode om een melding te ontvangen wanneer een bewerking is voltooid:

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

In tegenstelling tot WhenAll, dat de uitgepakte resultaten van alle taken die met succes zijn voltooid retourneert, retourneert WhenAny de taak die is voltooid. Als een taak mislukt, is het belangrijk om te weten dat deze is mislukt en als een taak slaagt, is het belangrijk om te weten met welke taak de retourwaarde is gekoppeld. Daarom moet u het resultaat van de geretourneerde taak openen of deze verder wachten, zoals in dit voorbeeld wordt weergegeven.

Net als bij WhenAll, moet u in staat zijn om uitzonderingen aan te kunnen. Omdat u de voltooide taak terug ontvangt, kunt u wachten op de geretourneerde taak om fouten door te geven en try/catch deze op de juiste manier worden doorgegeven, bijvoorbeeld:

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

Bovendien kunnen de volgende taken mislukken, zelfs als een eerste taak is voltooid. Op dit moment hebt u verschillende opties voor het verwerken van uitzonderingen: u kunt wachten totdat alle gestarte taken zijn voltooid. In dat geval kunt u de WhenAll methode gebruiken, of u kunt besluiten dat alle uitzonderingen belangrijk zijn en moeten worden geregistreerd. Voor dit scenario kunt u vervolgen gebruiken om een melding te ontvangen wanneer taken asynchroon worden voltooid:

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

of:

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

of zelfs:

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

Ten slotte wilt u mogelijk alle resterende bewerkingen annuleren:

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

Versnijden

Overweeg een geval waarin u afbeeldingen downloadt van het web en elke afbeelding verwerkt (bijvoorbeeld het toevoegen van de afbeelding aan een ui-besturingselement). U verwerkt de afbeeldingen sequentieel op de UI-thread, maar u wilt de afbeeldingen zo gelijktijdig mogelijk downloaden. U wilt ook niet wachten met het toevoegen van de afbeeldingen aan de user interface totdat ze allemaal zijn gedownload. In plaats daarvan wilt u ze toevoegen zodra ze zijn voltooid.

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

U kunt ook interleaving toepassen op een scenario waarbij rekenintensieve verwerking ThreadPool van de gedownloade afbeeldingen is betrokken, bijvoorbeeld:

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

Snelheidsbeperking

Bekijk het interleaving-voorbeeld, behalve dat de gebruiker zoveel afbeeldingen downloadt waarbij de downloads moeten worden beperkt. U wilt bijvoorbeeld dat er slechts een bepaald aantal downloads gelijktijdig plaatsvindt. Als u dit doel wilt bereiken, start u een subset van de asynchrone bewerkingen. Wanneer de bewerkingen zijn voltooid, kunt u extra bewerkingen starten om deze te vervangen.

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

Vroege borgtocht

Houd er rekening mee dat u asynchroon wacht totdat een bewerking is voltooid terwijl u tegelijkertijd reageert op de annuleringsaanvraag van een gebruiker (de gebruiker heeft bijvoorbeeld op een knop Annuleren geklikt). De volgende code illustreert dit 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

Met deze implementatie wordt de gebruikersinterface opnieuw ingeschakeld zodra u besluit om te stoppen, maar worden de onderliggende asynchrone bewerkingen niet geannuleerd. Een ander alternatief is het annuleren van de in behandeling zijnde bewerkingen wanneer u besluit om af te haken, maar de gebruikersinterface pas opnieuw opzetten nadat de bewerkingen zijn voltooid, waarbij ze mogelijk vroegtijdig eindigen als gevolg van de annuleringsaanvraag.

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

Een ander voorbeeld van vroege borgtocht omvat het gebruik van de WhenAny methode in combinatie met de Delay methode, zoals besproken in de volgende sectie.

Taakvertraging

Gebruik de Task.Delay methode om pauzes toe te voegen aan de uitvoering van een asynchrone methode. Deze pauze is handig voor veel soorten functionaliteit, waaronder het bouwen van polling-lussen en het vertragen van de verwerking van gebruikersinvoer gedurende een vooraf bepaalde periode. U kunt ook de Task.Delay methode gebruiken met Task.WhenAny om time-outs op awaits te implementeren.

Als een taak die deel uitmaakt van een grotere asynchrone bewerking (bijvoorbeeld een ASP.NET-webservice) te lang duurt, kan de algehele bewerking lijden, met name als deze niet kan worden voltooid. Daarom is het belangrijk dat u een time-out kunt instellen bij het wachten op een asynchrone bewerking. De synchrone Task.Wait, Task.WaitAll, en Task.WaitAny methoden accepteren time-outwaarden, maar de bijbehorende TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny en de eerder genoemde Task.WhenAll/Task.WhenAny methoden doen dat niet. Gebruik in plaats daarvan Task.Delay en Task.WhenAny samen om een time-out te implementeren.

Stel dat u in uw UI-applicatie een afbeelding wilt downloaden en de gebruikersinterface wilt uitschakelen terwijl de afbeelding wordt gedownload. Als het downloaden echter te lang duurt, wilt u de gebruikersinterface opnieuw inschakelen en de download negeren:

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

Hetzelfde principe is van toepassing op meerdere downloads, omdat WhenAll een taak wordt geretourneerd:

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

Op taken gebaseerde combinaties bouwen

Omdat een taak een asynchrone bewerking volledig kan vertegenwoordigen en synchrone en asynchrone mogelijkheden biedt voor het samenvoegen met de bewerking, het ophalen van de resultaten, enzovoort, kunt u nuttige bibliotheken bouwen van combinaties die taken samenstellen om grotere patronen te bouwen. Zoals besproken in de vorige sectie, bevat .NET verschillende ingebouwde combinaties, maar u kunt ook uw eigen combinaties maken. De volgende secties bevatten verschillende voorbeelden van mogelijke combinatiemethoden en -typen.

Opnieuw Proberen Bij Fout

In veel situaties wilt u een bewerking opnieuw proberen als een vorige poging mislukt. Voor synchrone code kunt u een helpermethode maken, zoals RetryOnFault in het volgende voorbeeld om deze taak uit te voeren:

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

U kunt een bijna identieke helpermethode bouwen voor asynchrone bewerkingen die zijn geïmplementeerd met TAP en dus taken retourneren:

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

U kunt deze combinatiefunctie vervolgens gebruiken om nieuwe pogingen te coderen in de logica van de toepassing. Voorbeeld:

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

U kunt de RetryOnFault functie verder uitbreiden. De functie kan bijvoorbeeld een andere Func<Task> functie accepteren die door de functie wordt aangeroepen tussen nieuwe pogingen om te bepalen wanneer de bewerking opnieuw moet worden uitgevoerd. Voorbeeld:

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

Vervolgens kunt u de functie als volgt gebruiken om een seconde te wachten voordat u de bewerking opnieuw probeert uit te voeren:

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

Soms kunt u profiteren van redundantie om de latentie en kansen voor succes van een bewerking te verbeteren. Overweeg meerdere webservices die aandelenkoersen bieden, maar op verschillende tijdstippen van de dag kunnen elke service verschillende niveaus van kwaliteit en reactietijden bieden. Als u deze schommelingen wilt afhandelen, kunt u aanvragen verzenden naar alle webservices en zodra u een reactie krijgt van de ene, annuleert u de resterende aanvragen. U kunt een helperfunctie implementeren om het eenvoudiger te maken om dit algemene patroon van het starten van meerdere bewerkingen te implementeren, te wachten op een en vervolgens de rest te annuleren. De NeedOnlyOne functie in het volgende voorbeeld illustreert dit 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

U kunt deze functie vervolgens als volgt gebruiken:

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

Interleaved-bewerkingen

Als u de WhenAny methode gebruikt om een interleavingsscenario te ondersteunen, kan dit een prestatieprobleem veroorzaken wanneer u met grote sets taken werkt. Elke aanroep naar WhenAny registreert een voortzetting bij elke taak. Voor N taken maakt dit proces O(N2) voortzettingen tijdens de levensduur van de interleaving operatie. Als u met een grote set taken werkt, gebruikt u een combinatie (Interleaved in het volgende voorbeeld) om het prestatieprobleem op te lossen:

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

Gebruik de combinator om de resultaten van taken te verwerken wanneer ze zijn voltooid. Voorbeeld:

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

WhenAllOrFirstException

In bepaalde spreiden/verzamelen scenario's wilt u mogelijk wachten op alle taken in een set, tenzij een daarvan een fout bevat. In dat geval wilt u stoppen met wachten zodra de uitzondering optreedt. U kunt dit gedrag uitvoeren met behulp van een combinatiemethode, zoals WhenAllOrFirstException in het volgende voorbeeld:

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

Op taken gebaseerde gegevensstructuren bouwen

Naast de mogelijkheid om aangepaste op taken gebaseerde combinaties te maken, met een gegevensstructuur in Task en Task<TResult> die zowel de resultaten van een asynchrone bewerking als de benodigde synchronisatie vertegenwoordigt om ermee samen te voegen, is het een krachtig type waarop aangepaste gegevensstructuren kunnen worden gebouwd die moeten worden gebruikt in asynchrone scenario's.

AsyncCache

Een belangrijk aspect van een taak is dat u deze aan meerdere consumenten kunt uitdelen. Alle consumenten kunnen erop wachten, voortzettingen registreren, het resultaat of de uitzonderingen ervan verkrijgen (in het geval van Task<TResult>) enzovoort. Dit aspect maakt Task en Task<TResult> perfect geschikt voor gebruik in een asynchrone cache-infrastructuur. Hier volgt een voorbeeld van een kleine, maar krachtige asynchrone cache die is gebouwd op 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

De AsyncCache<TKey,TValue-klasse> accepteert als delegeren naar zijn constructor een functie die een TKey neemt en een Task<TResult> retourneert. De interne woordenlijst slaat eerder geopende waarden op uit de cache en zorgt AsyncCache ervoor dat er slechts één taak per sleutel wordt gegenereerd, zelfs als de cache gelijktijdig wordt geopend.

U kunt bijvoorbeeld een cache bouwen voor gedownloade webpagina's:

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

U kunt deze cache vervolgens gebruiken in asynchrone methoden wanneer u de inhoud van een webpagina nodig hebt. De AsyncCache klasse zorgt ervoor dat u zo weinig mogelijk pagina's downloadt en de resultaten in de cache opgeslagen.

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

U kunt ook taken gebruiken om gegevensstructuren te bouwen voor het coördineren van asynchrone activiteiten. Overweeg een van de klassieke parallelle ontwerppatronen: producent/consument. In dit patroon genereren producenten gegevens die consumenten consumeren en kunnen de producenten en consumenten parallel werken. De consument verwerkt bijvoorbeeld item 1, dat eerder is gegenereerd door een producent die nu item 2 produceert. Voor het patroon producent/consument hebt u altijd een gegevensstructuur nodig om het werk op te slaan dat door producenten is gemaakt, zodat de consumenten op de hoogte kunnen worden gesteld van nieuwe gegevens en deze kunnen vinden wanneer ze beschikbaar zijn.

Hier volgt een eenvoudige gegevensstructuur, gebouwd op basis van taken, waarmee asynchrone methoden kunnen worden gebruikt als producenten en consumenten:

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

Nu deze gegevensstructuur is ingesteld, kunt u code schrijven, zoals de volgende:

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

De System.Threading.Tasks.Dataflow naamruimte bevat het BufferBlock<T> type dat u op een vergelijkbare manier kunt gebruiken, maar zonder dat u een aangepast verzamelingstype hoeft te maken:

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

Opmerking

De System.Threading.Tasks.Dataflow naamruimte is beschikbaar als een NuGet-pakket. Als u de assembly wilt installeren die de System.Threading.Tasks.Dataflow naamruimte bevat, opent u uw project in Visual Studio, kiest u NuGet-pakketten beheren in het menu Project en zoekt u online naar het System.Threading.Tasks.Dataflow pakket.

Zie ook